Use ApiKey for OpenAI Image/Audio/Moderation APIs

- Update to use ApiKey when instantiating OpenAI Image/Audio/Moderation APIs
 - Add Builders for OpenAI Image/Audio/Moderation APIs and deprecate existing constructors
 - Add/update tests
 - Replace hasText check with notNull for ApiKey with String value

Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
This commit is contained in:
Ilayaperumal Gopinathan
2025-02-12 10:41:37 +00:00
committed by Christian Tzolov
parent c19287d808
commit a528253ab0
20 changed files with 657 additions and 49 deletions

View File

@@ -186,9 +186,7 @@ public class OpenAiApi {
* @param restClientBuilder RestClient builder.
* @param webClientBuilder WebClient builder.
* @param responseErrorHandler Response error handler.
* @deprecated since 1.0.0.M6 - use {@link #builder()} instead
*/
@Deprecated(since = "1.0.0.M6")
public OpenAiApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers, String completionsPath,
String embeddingsPath, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler) {
@@ -1680,7 +1678,7 @@ public class OpenAiApi {
}
public Builder apiKey(String simpleApiKey) {
Assert.notNull(simpleApiKey, "apiKey cannot be null");
Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
this.apiKey = new SimpleApiKey(simpleApiKey);
return this;
}

View File

@@ -26,6 +26,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.core.io.ByteArrayResource;
@@ -45,6 +48,7 @@ import org.springframework.web.reactive.function.client.WebClient;
* <a href="https://platform.openai.com/docs/api-reference/audio">OpenAI Audio</a>
*
* @author Christian Tzolov
* @author Ilayaperumal Gopinathan
* @since 0.8.1
*/
public class OpenAiAudioApi {
@@ -56,7 +60,9 @@ public class OpenAiAudioApi {
/**
* Create a new audio api.
* @param openAiToken OpenAI apiKey.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiAudioApi(String openAiToken) {
this(OpenAiApiConstants.DEFAULT_BASE_URL, openAiToken, RestClient.builder(), WebClient.builder(),
RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
@@ -68,10 +74,11 @@ public class OpenAiAudioApi {
* @param openAiToken OpenAI apiKey.
* @param restClientBuilder RestClient builder.
* @param responseErrorHandler Response error handler.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiAudioApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
Consumer<HttpHeaders> authHeaders;
if (openAiToken != null && !openAiToken.isEmpty()) {
authHeaders = h -> h.setBearerAuth(openAiToken);
@@ -96,7 +103,9 @@ public class OpenAiAudioApi {
* @param restClientBuilder RestClient builder.
* @param webClientBuilder WebClient builder.
* @param responseErrorHandler Response error handler.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiAudioApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) {
@@ -112,14 +121,31 @@ public class OpenAiAudioApi {
* @param restClientBuilder RestClient builder.
* @param webClientBuilder WebClient builder.
* @param responseErrorHandler Response error handler.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiAudioApi(String baseUrl, String apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler) {
this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, webClientBuilder, responseErrorHandler);
}
/**
* Create a new audio api.
* @param baseUrl api base URL.
* @param apiKey OpenAI apiKey.
* @param headers the http headers to use.
* @param restClientBuilder RestClient builder.
* @param webClientBuilder WebClient builder.
* @param responseErrorHandler Response error handler.
*/
public OpenAiAudioApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler) {
Consumer<HttpHeaders> authHeaders = h -> {
if (apiKey != null && !apiKey.isEmpty()) {
h.setBearerAuth(apiKey);
if (!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.addAll(headers);
// h.setContentType(MediaType.APPLICATION_JSON);
@@ -133,6 +159,10 @@ public class OpenAiAudioApi {
this.webClient = webClientBuilder.baseUrl(baseUrl).defaultHeaders(authHeaders).build();
}
public static Builder builder() {
return new Builder();
}
/**
* Request to generates audio from the input text.
* @param requestBody The request body.
@@ -932,4 +962,71 @@ public class OpenAiAudioApi {
}
/**
* Builder to construct {@link OpenAiAudioApi} instance.
*/
public static class Builder {
private String baseUrl = OpenAiApiConstants.DEFAULT_BASE_URL;
private ApiKey apiKey;
private MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
private RestClient.Builder restClientBuilder = RestClient.builder();
private WebClient.Builder webClientBuilder = WebClient.builder();
private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
public Builder baseUrl(String baseUrl) {
Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
this.baseUrl = baseUrl;
return this;
}
public Builder apiKey(ApiKey apiKey) {
Assert.notNull(apiKey, "apiKey cannot be null");
this.apiKey = apiKey;
return this;
}
public Builder apiKey(String simpleApiKey) {
Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
this.apiKey = new SimpleApiKey(simpleApiKey);
return this;
}
public Builder headers(MultiValueMap<String, String> headers) {
Assert.notNull(headers, "headers cannot be null");
this.headers = headers;
return this;
}
public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
this.restClientBuilder = restClientBuilder;
return this;
}
public Builder webClientBuilder(WebClient.Builder webClientBuilder) {
Assert.notNull(webClientBuilder, "webClientBuilder cannot be null");
this.webClientBuilder = webClientBuilder;
return this;
}
public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
this.responseErrorHandler = responseErrorHandler;
return this;
}
public OpenAiAudioApi build() {
Assert.notNull(this.apiKey, "apiKey must be set");
return new OpenAiAudioApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder,
this.webClientBuilder, this.responseErrorHandler);
}
}
}

View File

@@ -22,12 +22,16 @@ import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
@@ -46,7 +50,9 @@ public class OpenAiImageApi {
/**
* Create a new OpenAI Image api with base URL set to {@code https://api.openai.com}.
* @param openAiToken OpenAI apiKey.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiImageApi(String openAiToken) {
this(OpenAiApiConstants.DEFAULT_BASE_URL, openAiToken, RestClient.builder());
}
@@ -55,8 +61,9 @@ public class OpenAiImageApi {
* Create a new OpenAI Image API with the provided base URL.
* @param baseUrl the base URL for the OpenAI API.
* @param openAiToken OpenAI apiKey.
* @param restClientBuilder the rest client builder to use.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiImageApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder) {
this(baseUrl, openAiToken, restClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
}
@@ -66,8 +73,9 @@ public class OpenAiImageApi {
* @param baseUrl the base URL for the OpenAI API.
* @param apiKey OpenAI apiKey.
* @param restClientBuilder the rest client builder to use.
* @param responseErrorHandler the response error handler to use.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, responseErrorHandler);
@@ -80,15 +88,31 @@ public class OpenAiImageApi {
* @param headers the http headers to use.
* @param restClientBuilder the rest client builder to use.
* @param responseErrorHandler the response error handler to use.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiImageApi(String baseUrl, String apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, responseErrorHandler);
}
/**
* Create a new OpenAI Image API with the provided base URL.
* @param baseUrl the base URL for the OpenAI API.
* @param apiKey OpenAI apiKey.
* @param headers the http headers to use.
* @param restClientBuilder the rest client builder to use.
* @param responseErrorHandler the response error handler to use.
*/
public OpenAiImageApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// @formatter:off
this.restClient = restClientBuilder.baseUrl(baseUrl)
.defaultHeaders(h -> {
if(apiKey != null && !apiKey.isEmpty()) {
h.setBearerAuth(apiKey);
if(!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
@@ -169,4 +193,67 @@ public class OpenAiImageApi {
}
public static Builder builder() {
return new Builder();
}
/**
* Builder to construct {@link OpenAiImageApi} instance.
*/
public static class Builder {
private String baseUrl = OpenAiApiConstants.DEFAULT_BASE_URL;
private ApiKey apiKey;
private MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
private RestClient.Builder restClientBuilder = RestClient.builder();
private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
public Builder baseUrl(String baseUrl) {
Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
this.baseUrl = baseUrl;
return this;
}
public Builder apiKey(ApiKey apiKey) {
Assert.notNull(apiKey, "apiKey cannot be null");
this.apiKey = apiKey;
return this;
}
public Builder apiKey(String simpleApiKey) {
Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
this.apiKey = new SimpleApiKey(simpleApiKey);
return this;
}
public Builder headers(MultiValueMap<String, String> headers) {
Assert.notNull(headers, "headers cannot be null");
this.headers = headers;
return this;
}
public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
this.restClientBuilder = restClientBuilder;
return this;
}
public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
this.responseErrorHandler = responseErrorHandler;
return this;
}
public OpenAiImageApi build() {
Assert.notNull(this.apiKey, "apiKey must be set");
return new OpenAiImageApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder,
this.responseErrorHandler);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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,10 +20,17 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
@@ -31,6 +38,7 @@ import org.springframework.web.client.RestClient;
* OpenAI Moderation API.
*
* @author Ahmed Yousri
* @author Ilayaperumal Gopinathan
* @see <a href=
* "https://platform.openai.com/docs/api-reference/moderations">https://platform.openai.com/docs/api-reference/moderations</a>
*/
@@ -47,11 +55,17 @@ public class OpenAiModerationApi {
/**
* Create a new OpenAI Moderation api with base URL set to https://api.openai.com
* @param openAiToken OpenAI apiKey.
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiModerationApi(String openAiToken) {
this(DEFAULT_BASE_URL, openAiToken, RestClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
}
/**
* @deprecated use {@link Builder} instead.
*/
@Deprecated(forRemoval = true, since = "1.0.0-M6")
public OpenAiModerationApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
@@ -65,6 +79,26 @@ public class OpenAiModerationApi {
}).defaultStatusHandler(responseErrorHandler).build();
}
/**
* Create a new OpenAI Moderation API with the provided base URL.
* @param baseUrl the base URL for the OpenAI API.
* @param apiKey OpenAI apiKey.
* @param restClientBuilder the rest client builder to use.
*/
public OpenAiModerationApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(h -> {
if (!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
}).defaultStatusHandler(responseErrorHandler).build();
}
public ResponseEntity<OpenAiModerationResponse> createModeration(OpenAiModerationRequest openAiModerationRequest) {
Assert.notNull(openAiModerationRequest, "Moderation request cannot be null.");
Assert.hasLength(openAiModerationRequest.prompt(), "Prompt cannot be empty.");
@@ -142,4 +176,67 @@ public class OpenAiModerationApi {
}
public static Builder builder() {
return new Builder();
}
/**
* Builder to construct {@link OpenAiModerationApi} instance.
*/
public static class Builder {
private String baseUrl = OpenAiApiConstants.DEFAULT_BASE_URL;
private ApiKey apiKey;
private MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
private RestClient.Builder restClientBuilder = RestClient.builder();
private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
public Builder baseUrl(String baseUrl) {
Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
this.baseUrl = baseUrl;
return this;
}
public Builder apiKey(ApiKey apiKey) {
Assert.notNull(apiKey, "apiKey cannot be null");
this.apiKey = apiKey;
return this;
}
public Builder apiKey(String simpleApiKey) {
Assert.notNull(simpleApiKey, "simpleApiKey cannot be null");
this.apiKey = new SimpleApiKey(simpleApiKey);
return this;
}
public Builder headers(MultiValueMap<String, String> headers) {
Assert.notNull(headers, "headers cannot be null");
this.headers = headers;
return this;
}
public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
this.restClientBuilder = restClientBuilder;
return this;
}
public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
this.responseErrorHandler = responseErrorHandler;
return this;
}
public OpenAiModerationApi build() {
Assert.notNull(this.apiKey, "apiKey must be set");
return new OpenAiModerationApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder,
this.responseErrorHandler);
}
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.ai.openai;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
import org.springframework.ai.openai.api.OpenAiAudioApi;
@@ -30,31 +32,31 @@ public class OpenAiTestConfiguration {
@Bean
public OpenAiApi openAiApi() {
return new OpenAiApi(getApiKey());
return OpenAiApi.builder().apiKey(getApiKey()).build();
}
@Bean
public OpenAiImageApi openAiImageApi() {
return new OpenAiImageApi(getApiKey());
return OpenAiImageApi.builder().apiKey(getApiKey()).build();
}
@Bean
public OpenAiAudioApi openAiAudioApi() {
return new OpenAiAudioApi(getApiKey());
return OpenAiAudioApi.builder().apiKey(getApiKey()).build();
}
@Bean
public OpenAiModerationApi openAiModerationApi() {
return new OpenAiModerationApi(getApiKey());
return OpenAiModerationApi.builder().apiKey(getApiKey()).build();
}
private String getApiKey() {
private ApiKey getApiKey() {
String apiKey = System.getenv("OPENAI_API_KEY");
if (!StringUtils.hasText(apiKey)) {
throw new IllegalArgumentException(
"You must provide an API key. Put it in an environment variable under the name OPENAI_API_KEY");
}
return apiKey;
return new SimpleApiKey(apiKey);
}
@Bean

View File

@@ -19,6 +19,7 @@ package org.springframework.ai.openai;
import org.junit.jupiter.api.Test;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.api.OpenAiAudioApi.TranscriptResponseFormat;
import org.springframework.ai.openai.api.OpenAiAudioApi.TranscriptionRequest.GranularityType;
@@ -35,7 +36,8 @@ public class TranscriptionRequestTests {
@Test
public void defaultOptions() {
var client = new OpenAiAudioTranscriptionModel(new OpenAiAudioApi("TEST"),
var client = new OpenAiAudioTranscriptionModel(
OpenAiAudioApi.builder().apiKey(new SimpleApiKey("TEST")).build(),
OpenAiAudioTranscriptionOptions.builder()
.model("DEFAULT_MODEL")
.responseFormat(TranscriptResponseFormat.TEXT)
@@ -59,7 +61,8 @@ public class TranscriptionRequestTests {
@Test
public void runtimeOptions() {
var client = new OpenAiAudioTranscriptionModel(new OpenAiAudioApi("TEST"),
var client = new OpenAiAudioTranscriptionModel(
OpenAiAudioApi.builder().apiKey(new SimpleApiKey("TEST")).build(),
OpenAiAudioTranscriptionOptions.builder()
.model("DEFAULT_MODEL")
.responseFormat(TranscriptResponseFormat.TEXT)

View File

@@ -22,6 +22,7 @@ import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.api.OpenAiAudioApi.SpeechRequest;
import org.springframework.ai.openai.api.OpenAiAudioApi.SpeechRequest.Voice;
@@ -40,7 +41,9 @@ import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiAudioApiIT {
OpenAiAudioApi audioApi = new OpenAiAudioApi(System.getenv("OPENAI_API_KEY"));
OpenAiAudioApi audioApi = OpenAiAudioApi.builder()
.apiKey(new SimpleApiKey(System.getenv("OPENAI_API_KEY")))
.build();
@SuppressWarnings("null")
@Test

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025-2025 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.openai.audio.api;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.retry.NonTransientAiException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
/**
* @author Ilayaperumal Gopinathan
*/
@SpringBootTest(classes = OpenAiAudioModelNoOpApiKeysIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiAudioModelNoOpApiKeysIT {
@Autowired
private OpenAiAudioApi audioApi;
@Test
void checkNoOpKey() {
assertThatThrownBy(() -> {
this.audioApi
.createSpeech(OpenAiAudioApi.SpeechRequest.builder()
.model(OpenAiAudioApi.TtsModel.TTS_1_HD.getValue())
.input("Hello, my name is Chris and I love Spring A.I.")
.voice(OpenAiAudioApi.SpeechRequest.Voice.ONYX)
.build())
.getBody();
}).isInstanceOf(NonTransientAiException.class);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiAudioApi openAiAudioApi() {
return OpenAiAudioApi.builder().apiKey(new NoopApiKey()).build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 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.
@@ -18,15 +18,16 @@ package org.springframework.ai.openai.audio.speech;
import java.time.Duration;
import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.OpenAiAudioSpeechOptions;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.metadata.audio.OpenAiAudioSpeechResponseMetadata;
import org.springframework.ai.openai.metadata.support.OpenAiApiResponseHeaders;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
@@ -38,6 +39,8 @@ import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.matches;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
@@ -111,7 +114,7 @@ public class OpenAiSpeechModelWithSpeechResponseMetadataTests {
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_RESET_HEADER.getName(), "27h55s451ms");
httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
this.server.expect(requestTo("/v1/audio/speech"))
this.server.expect(requestTo(StringContains.containsString("/v1/audio/speech")))
.andExpect(method(HttpMethod.POST))
.andExpect(header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_API_KEY))
.andRespond(withSuccess("Audio bytes as string", MediaType.APPLICATION_OCTET_STREAM).headers(httpHeaders));
@@ -128,7 +131,7 @@ public class OpenAiSpeechModelWithSpeechResponseMetadataTests {
@Bean
public OpenAiAudioApi openAiAudioApi(RestClient.Builder builder) {
return new OpenAiAudioApi("", TEST_API_KEY, builder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
return OpenAiAudioApi.builder().apiKey(new SimpleApiKey(TEST_API_KEY)).restClientBuilder(builder).build();
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.ai.openai.audio.transcription;
import java.time.Duration;
import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -25,11 +26,11 @@ import org.springframework.ai.audio.transcription.AudioTranscriptionMetadata;
import org.springframework.ai.audio.transcription.AudioTranscriptionPrompt;
import org.springframework.ai.audio.transcription.AudioTranscriptionResponse;
import org.springframework.ai.chat.metadata.RateLimit;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.metadata.audio.OpenAiAudioTranscriptionResponseMetadata;
import org.springframework.ai.openai.metadata.support.OpenAiApiResponseHeaders;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
@@ -120,7 +121,7 @@ public class OpenAiTranscriptionModelWithTranscriptionResponseMetadataTests {
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_REMAINING_HEADER.getName(), "112358");
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_RESET_HEADER.getName(), "27h55s451ms");
this.server.expect(requestTo("/v1/audio/transcriptions"))
this.server.expect(requestTo(StringContains.containsString("/v1/audio/transcriptions")))
.andExpect(method(HttpMethod.POST))
.andExpect(header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_API_KEY))
.andRespond(withSuccess(getJson(), MediaType.APPLICATION_JSON).headers(httpHeaders));
@@ -156,7 +157,7 @@ public class OpenAiTranscriptionModelWithTranscriptionResponseMetadataTests {
@Bean
public OpenAiAudioApi chatCompletionApi(RestClient.Builder builder) {
return new OpenAiAudioApi("", TEST_API_KEY, builder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
return OpenAiAudioApi.builder().apiKey(new SimpleApiKey(TEST_API_KEY)).restClientBuilder(builder).build();
}
@Bean

View File

@@ -23,6 +23,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
@@ -38,9 +39,9 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
/**
* @author Christian Tzolov
*/
@SpringBootTest(classes = OpenAiChatModeAdditionalHttpHeadersIT.Config.class)
@SpringBootTest(classes = OpenAiChatModelAdditionalHttpHeadersIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiChatModeAdditionalHttpHeadersIT {
public class OpenAiChatModelAdditionalHttpHeadersIT {
@Autowired
private OpenAiChatModel openAiChatModel;
@@ -67,7 +68,7 @@ public class OpenAiChatModeAdditionalHttpHeadersIT {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi("Invalid API Key");
return OpenAiApi.builder().apiKey(new SimpleApiKey("Invalid API Key")).build();
}
@Bean

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025-2025 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.openai.chat;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.retry.NonTransientAiException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
/**
* @author Ilayaperumal Gopinathan
*/
@SpringBootTest(classes = OpenAiChatModelNoOpApiKeysIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiChatModelNoOpApiKeysIT {
@Autowired
private OpenAiChatModel openAiChatModel;
@Test
void checkNoOpApiKey() {
assertThatThrownBy(() -> this.openAiChatModel.call("Tell me a joke"))
.isInstanceOf(NonTransientAiException.class);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return OpenAiApi.builder().apiKey(new NoopApiKey()).build();
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
}
}

View File

@@ -391,7 +391,10 @@ class MistralWithOpenAiChatModelIT {
@Bean
public OpenAiApi chatCompletionApi() {
return OpenAiApi.builder().baseUrl(MISTRAL_BASE_URL).apiKey(System.getenv("MISTRAL_AI_API_KEY")).build();
return OpenAiApi.builder()
.baseUrl(MISTRAL_BASE_URL)
.apiKey(new SimpleApiKey(System.getenv("MISTRAL_AI_API_KEY")))
.build();
}
@Bean

View File

@@ -49,6 +49,8 @@ import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.ListOutputConverter;
import org.springframework.ai.converter.MapOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.LegacyToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel;
@@ -413,7 +415,7 @@ class OllamaWithOpenAiChatModelIT {
@Bean
public OpenAiApi chatCompletionApi() {
return OpenAiApi.builder().baseUrl(baseUrl).apiKey("").build();
return OpenAiApi.builder().baseUrl(baseUrl).apiKey(new NoopApiKey()).build();
}
@Bean

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025-2025 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.openai.image;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.image.ImageOptionsBuilder;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.retry.NonTransientAiException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
/**
* @author Ilayaperumal Gopinathan
*/
@SpringBootTest(classes = OpenAiImageModelNoOpApiKeysIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiImageModelNoOpApiKeysIT {
@Autowired
private OpenAiImageModel imageModel;
@Test
void checkNoOpKey() {
assertThatThrownBy(() -> {
var options = ImageOptionsBuilder.builder().height(1024).width(1024).build();
var instructions = """
A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!".""";
ImagePrompt imagePrompt = new ImagePrompt(instructions, options);
this.imageModel.call(imagePrompt);
}).isInstanceOf(NonTransientAiException.class);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiImageApi openAiImageApi() {
return OpenAiImageApi.builder().apiKey(new NoopApiKey()).build();
}
@Bean
public OpenAiImageModel openAiImageModel(OpenAiImageApi openAiImageApi) {
return new OpenAiImageModel(openAiImageApi, OpenAiImageOptions.builder().build(),
RetryTemplate.defaultInstance(), TestObservationRegistry.create());
}
}
}

View File

@@ -24,6 +24,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.openai.OpenAiImageModel;
@@ -97,7 +98,7 @@ public class OpenAiImageModelObservationIT {
@Bean
public OpenAiImageApi openAiImageApi() {
return new OpenAiImageApi(System.getenv("OPENAI_API_KEY"));
return OpenAiImageApi.builder().apiKey(new SimpleApiKey(System.getenv("OPENAI_API_KEY"))).build();
}
@Bean

View File

@@ -18,6 +18,7 @@ package org.springframework.ai.openai.image;
import java.util.List;
import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -25,8 +26,10 @@ import org.springframework.ai.image.ImageGeneration;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.openai.metadata.support.OpenAiApiResponseHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
@@ -104,7 +107,7 @@ public class OpenAiImageModelWithImageResponseMetadataTests {
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_REMAINING_HEADER.getName(), "112358");
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_RESET_HEADER.getName(), "27h55s451ms");
this.server.expect(requestTo("v1/images/generations"))
this.server.expect(requestTo(StringContains.containsString("v1/images/generations")))
.andExpect(method(HttpMethod.POST))
.andExpect(header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_API_KEY))
.andRespond(withSuccess(getJson(), MediaType.APPLICATION_JSON).headers(httpHeaders));
@@ -132,7 +135,7 @@ public class OpenAiImageModelWithImageResponseMetadataTests {
@Bean
public OpenAiImageApi imageGenerationApi(RestClient.Builder builder) {
return new OpenAiImageApi("", TEST_API_KEY, builder);
return OpenAiImageApi.builder().apiKey(new SimpleApiKey(TEST_API_KEY)).restClientBuilder(builder).build();
}
@Bean

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025-2025 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.openai.moderation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.moderation.ModerationPrompt;
import org.springframework.ai.openai.OpenAiModerationModel;
import org.springframework.ai.openai.api.OpenAiModerationApi;
import org.springframework.ai.retry.NonTransientAiException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
/**
* @author Ilayaperumal Gopinathan
*/
@SpringBootTest(classes = OpenAiModerationModelNoOpApiKeysIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiModerationModelNoOpApiKeysIT {
@Autowired
private OpenAiModerationModel moderationModel;
@Test
void checkNoOpKey() {
assertThatThrownBy(() -> {
ModerationPrompt prompt = new ModerationPrompt("I want to kill them..");
this.moderationModel.call(prompt);
}).isInstanceOf(NonTransientAiException.class);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiModerationApi moderationGenerationApi() {
return OpenAiModerationApi.builder().apiKey(new NoopApiKey()).build();
}
@Bean
public OpenAiModerationModel openAiModerationClient(OpenAiModerationApi openAiModerationApi) {
return new OpenAiModerationModel(openAiModerationApi);
}
}
}

View File

@@ -18,9 +18,12 @@ package org.springframework.ai.openai.moderation;
import java.util.List;
import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.moderation.Categories;
import org.springframework.ai.moderation.CategoryScores;
import org.springframework.ai.moderation.Generation;
@@ -130,7 +133,7 @@ public class OpenAiModerationModelTests {
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_REMAINING_HEADER.getName(), "112358");
httpHeaders.set(OpenAiApiResponseHeaders.TOKENS_RESET_HEADER.getName(), "27h55s451ms");
this.server.expect(requestTo("v1/moderations"))
this.server.expect(requestTo(StringContains.containsString("v1/moderations")))
.andExpect(method(HttpMethod.POST))
.andExpect(header(HttpHeaders.AUTHORIZATION, "Bearer " + TEST_API_KEY))
.andRespond(withSuccess(getJson(), MediaType.APPLICATION_JSON).headers(httpHeaders));
@@ -182,7 +185,11 @@ public class OpenAiModerationModelTests {
@Bean
public OpenAiModerationApi moderationGenerationApi(RestClient.Builder builder) {
return new OpenAiModerationApi("", TEST_API_KEY, builder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
return OpenAiModerationApi.builder()
.apiKey(new SimpleApiKey(TEST_API_KEY))
.restClientBuilder(builder)
.responseErrorHandler(RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER)
.build();
}
@Bean

View File

@@ -68,6 +68,7 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Christian Tzolov
* @author Stefan Vassilev
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
@@ -210,9 +211,13 @@ public class OpenAiAutoConfiguration {
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image");
var openAiImageApi = new OpenAiImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var openAiImageApi = OpenAiImageApi.builder()
.baseUrl(resolved.baseUrl())
.apiKey(new SimpleApiKey(resolved.apiKey()))
.headers(resolved.headers())
.restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
.responseErrorHandler(responseErrorHandler)
.build();
var imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
@@ -233,9 +238,14 @@ public class OpenAiAutoConfiguration {
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, transcriptionProperties,
"transcription");
var openAiAudioApi = new OpenAiAudioApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
restClientBuilderProvider.getIfAvailable(RestClient::builder),
webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler);
var openAiAudioApi = OpenAiAudioApi.builder()
.baseUrl(resolved.baseUrl())
.apiKey(new SimpleApiKey(resolved.apiKey()))
.headers(resolved.headers())
.restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
.webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder))
.responseErrorHandler(responseErrorHandler)
.build();
return new OpenAiAudioTranscriptionModel(openAiAudioApi, transcriptionProperties.getOptions(), retryTemplate);
@@ -250,9 +260,13 @@ public class OpenAiAutoConfiguration {
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, moderationProperties,
"moderation");
var openAiModerationApi = new OpenAiModerationApi(resolved.baseUrl, resolved.apiKey(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var openAiModerationApi = OpenAiModerationApi.builder()
.baseUrl(resolved.baseUrl)
.apiKey(new SimpleApiKey(resolved.apiKey()))
.headers(resolved.headers())
.restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
.responseErrorHandler(responseErrorHandler)
.build();
return new OpenAiModerationModel(openAiModerationApi, retryTemplate)
.withDefaultOptions(moderationProperties.getOptions());
}
@@ -269,9 +283,14 @@ public class OpenAiAutoConfiguration {
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechProperties,
"speach");
var openAiAudioApi = new OpenAiAudioApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
restClientBuilderProvider.getIfAvailable(RestClient::builder),
webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler);
var openAiAudioApi = OpenAiAudioApi.builder()
.baseUrl(resolved.baseUrl())
.apiKey(new SimpleApiKey(resolved.apiKey()))
.headers(resolved.headers())
.restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
.webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder))
.responseErrorHandler(responseErrorHandler)
.build();
return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions());
}