Skip to content

Commit a18c814

Browse files
committed
OpenAI: moderation model support moderationPath configuration
Signed-off-by: lambochen <[email protected]>
1 parent fdbf737 commit a18c814

File tree

5 files changed

+74
-15
lines changed

5 files changed

+74
-15
lines changed

auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* @author Stefan Vassilev
4747
* @author Thomas Vitale
4848
* @author Ilayaperumal Gopinathan
49+
* @author lambochen
4950
*/
5051
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
5152
SpringAiRetryAutoConfiguration.class })
@@ -68,6 +69,7 @@ public OpenAiModerationModel openAiModerationModel(OpenAiConnectionProperties co
6869

6970
var openAiModerationApi = OpenAiModerationApi.builder()
7071
.baseUrl(resolved.baseUrl())
72+
.moderationPath(moderationProperties.getModerationPath())
7173
.apiKey(new SimpleApiKey(resolved.apiKey()))
7274
.headers(resolved.headers())
7375
.restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))

auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package org.springframework.ai.model.openai.autoconfigure;
1818

1919
import org.springframework.ai.openai.OpenAiModerationOptions;
20+
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
2021
import org.springframework.boot.context.properties.ConfigurationProperties;
2122
import org.springframework.boot.context.properties.NestedConfigurationProperty;
2223

2324
/**
2425
* OpenAI Moderation autoconfiguration properties.
2526
*
2627
* @author Ahmed Yousri
28+
* @author lambochen
2729
* @since 0.9.0
2830
*/
2931
@ConfigurationProperties(OpenAiModerationProperties.CONFIG_PREFIX)
@@ -37,6 +39,8 @@ public class OpenAiModerationProperties extends OpenAiParentProperties {
3739
@NestedConfigurationProperty
3840
private OpenAiModerationOptions options = OpenAiModerationOptions.builder().build();
3941

42+
private String moderationPath = OpenAiApiConstants.DEFAULT_MODERATION_PATH;
43+
4044
public OpenAiModerationOptions getOptions() {
4145
return this.options;
4246
}
@@ -45,4 +49,12 @@ public void setOptions(OpenAiModerationOptions options) {
4549
this.options = options;
4650
}
4751

52+
public String getModerationPath() {
53+
return moderationPath;
54+
}
55+
56+
public void setModerationPath(String moderationPath) {
57+
this.moderationPath = moderationPath;
58+
}
59+
4860
}

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiModerationApi.java

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.springframework.ai.model.ApiKey;
2626
import org.springframework.ai.model.NoopApiKey;
2727
import org.springframework.ai.model.SimpleApiKey;
28+
import org.springframework.ai.moderation.Categories;
29+
import org.springframework.ai.moderation.CategoryScores;
2830
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
2931
import org.springframework.ai.retry.RetryUtils;
3032
import org.springframework.http.HttpHeaders;
@@ -42,6 +44,7 @@
4244
* @author Ahmed Yousri
4345
* @author Ilayaperumal Gopinathan
4446
* @author Filip Hrisafov
47+
* @author lambochen
4548
* @see <a href=
4649
* "https://platform.openai.com/docs/api-reference/moderations">https://platform.openai.com/docs/api-reference/moderations</a>
4750
*/
@@ -55,6 +58,8 @@ public class OpenAiModerationApi {
5558

5659
private final ObjectMapper objectMapper;
5760

61+
private final String moderationPath;
62+
5863
/**
5964
* Create a new OpenAI Moderation API with the provided base URL.
6065
* @param baseUrl the base URL for the OpenAI API.
@@ -63,31 +68,47 @@ public class OpenAiModerationApi {
6368
*/
6469
public OpenAiModerationApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
6570
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
71+
this(baseUrl, OpenAiApiConstants.DEFAULT_MODERATION_PATH, apiKey, headers, restClientBuilder,
72+
responseErrorHandler);
73+
}
6674

75+
/**
76+
* Create a new OpenAI Moderation API with the provided base URL.
77+
* @param baseUrl the base URL for the OpenAI API.
78+
* @param apiKey OpenAI apiKey.
79+
* @param restClientBuilder the rest client builder to use.
80+
* @param moderationPath the moderation path to use.
81+
*/
82+
public OpenAiModerationApi(String baseUrl, String moderationPath, ApiKey apiKey,
83+
MultiValueMap<String, String> headers, RestClient.Builder restClientBuilder,
84+
ResponseErrorHandler responseErrorHandler) {
85+
Assert.hasText(moderationPath, "moderationPath cannot be null or empty");
86+
87+
this.moderationPath = moderationPath;
6788
this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
6889

6990
// @formatter:off
7091
this.restClient = restClientBuilder.clone()
71-
.baseUrl(baseUrl)
72-
.defaultHeaders(h -> {
73-
h.setContentType(MediaType.APPLICATION_JSON);
74-
h.addAll(headers);
75-
})
76-
.defaultStatusHandler(responseErrorHandler)
77-
.defaultRequest(requestHeadersSpec -> {
78-
if (!(apiKey instanceof NoopApiKey)) {
79-
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
80-
}
81-
})
82-
.build(); // @formatter:on
92+
.baseUrl(baseUrl)
93+
.defaultHeaders(h -> {
94+
h.setContentType(MediaType.APPLICATION_JSON);
95+
h.addAll(headers);
96+
})
97+
.defaultStatusHandler(responseErrorHandler)
98+
.defaultRequest(requestHeadersSpec -> {
99+
if (!(apiKey instanceof NoopApiKey)) {
100+
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
101+
}
102+
})
103+
.build(); // @formatter:on
83104
}
84105

85106
public ResponseEntity<OpenAiModerationResponse> createModeration(OpenAiModerationRequest openAiModerationRequest) {
86107
Assert.notNull(openAiModerationRequest, "Moderation request cannot be null.");
87108
Assert.hasLength(openAiModerationRequest.prompt(), "Prompt cannot be empty.");
88109

89110
return this.restClient.post()
90-
.uri("v1/moderations")
111+
.uri(this.moderationPath)
91112
.body(openAiModerationRequest)
92113
.retrieve()
93114
.toEntity(OpenAiModerationResponse.class);
@@ -176,6 +197,8 @@ public static class Builder {
176197

177198
private String baseUrl = OpenAiApiConstants.DEFAULT_BASE_URL;
178199

200+
private String moderationPath = OpenAiApiConstants.DEFAULT_MODERATION_PATH;
201+
179202
private ApiKey apiKey;
180203

181204
private MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
@@ -190,6 +213,12 @@ public Builder baseUrl(String baseUrl) {
190213
return this;
191214
}
192215

216+
public Builder moderationPath(String moderationPath) {
217+
Assert.hasText(moderationPath, "moderationPath cannot be null or empty");
218+
this.moderationPath = moderationPath;
219+
return this;
220+
}
221+
193222
public Builder apiKey(ApiKey apiKey) {
194223
Assert.notNull(apiKey, "apiKey cannot be null");
195224
this.apiKey = apiKey;
@@ -222,8 +251,8 @@ public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
222251

223252
public OpenAiModerationApi build() {
224253
Assert.notNull(this.apiKey, "apiKey must be set");
225-
return new OpenAiModerationApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder,
226-
this.responseErrorHandler);
254+
return new OpenAiModerationApi(this.baseUrl, this.moderationPath, this.apiKey, this.headers,
255+
this.restClientBuilder, this.responseErrorHandler);
227256
}
228257

229258
}

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/common/OpenAiApiConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public final class OpenAiApiConstants {
3636

3737
public static final String DEFAULT_AUDIO_TRANSLATION_PATH = "/v1/audio/translations";
3838

39+
public static final String DEFAULT_MODERATION_PATH = "v1/moderations";
40+
3941
public static final String PROVIDER_NAME = AiProvider.OPENAI.value();
4042

4143
private OpenAiApiConstants() {

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/moderation/api/OpenAiModerationApiBuilderTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ class OpenAiModerationApiBuilderTests {
5555

5656
private static final String TEST_BASE_URL = "https://test.openai.com";
5757

58+
private static final String TEST_MODERATION_PATH = "/v1/moderations";
59+
5860
@Test
5961
void testMinimalBuilder() {
6062
OpenAiModerationApi api = OpenAiModerationApi.builder().apiKey(TEST_API_KEY).build();
@@ -71,6 +73,7 @@ void testFullBuilder() {
7173

7274
OpenAiModerationApi api = OpenAiModerationApi.builder()
7375
.baseUrl(TEST_BASE_URL)
76+
.moderationPath(TEST_MODERATION_PATH)
7477
.apiKey(TEST_API_KEY)
7578
.headers(headers)
7679
.restClientBuilder(restClientBuilder)
@@ -97,6 +100,17 @@ void testInvalidBaseUrl() {
97100
.hasMessageContaining("baseUrl cannot be null or empty");
98101
}
99102

103+
@Test
104+
void testInvalidModerationPath() {
105+
assertThatThrownBy(() -> OpenAiModerationApi.builder().moderationPath("").build())
106+
.isInstanceOf(IllegalArgumentException.class)
107+
.hasMessageContaining("moderationPath cannot be null or empty");
108+
109+
assertThatThrownBy(() -> OpenAiModerationApi.builder().moderationPath(null).build())
110+
.isInstanceOf(IllegalArgumentException.class)
111+
.hasMessageContaining("moderationPath cannot be null or empty");
112+
}
113+
100114
@Test
101115
void testInvalidHeaders() {
102116
assertThatThrownBy(() -> OpenAiModerationApi.builder().headers(null).build())

0 commit comments

Comments
 (0)