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:
committed by
Christian Tzolov
parent
c19287d808
commit
a528253ab0
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user