Revert "Resolve OpenAI ApiKey for every request"

This reverts commit 3a527eeb98.
This commit is contained in:
Ilayaperumal Gopinathan
2025-05-30 12:54:05 +01:00
parent 72fe3bfd5e
commit 313aae0e89
8 changed files with 20 additions and 752 deletions

View File

@@ -62,7 +62,6 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Thomas Vitale
* @author David Frizelle
* @author Alexandros Pappas
* @author Filip Hrisafov
*/
public class OpenAiApi {
@@ -129,6 +128,10 @@ public class OpenAiApi {
// @formatter:off
Consumer<HttpHeaders> finalHeaders = h -> {
if (!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
};
@@ -136,21 +139,11 @@ public class OpenAiApi {
.baseUrl(baseUrl)
.defaultHeaders(finalHeaders)
.defaultStatusHandler(responseErrorHandler)
.defaultRequest(requestHeadersSpec -> {
if (!(apiKey instanceof NoopApiKey)) {
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
}
})
.build();
this.webClient = webClientBuilder.clone()
.baseUrl(baseUrl)
.defaultHeaders(finalHeaders)
.defaultRequest(requestHeadersSpec -> {
if (!(apiKey instanceof NoopApiKey)) {
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
}
})
.build(); // @formatter:on
}

View File

@@ -49,7 +49,6 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Christian Tzolov
* @author Ilayaperumal Gopinathan
* @author Jonghoon Park
* @author Filip Hrisafov
* @since 0.8.1
*/
public class OpenAiAudioApi {
@@ -72,30 +71,20 @@ public class OpenAiAudioApi {
ResponseErrorHandler responseErrorHandler) {
Consumer<HttpHeaders> authHeaders = h -> {
if (!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.addAll(headers);
// h.setContentType(MediaType.APPLICATION_JSON);
};
// @formatter:off
this.restClient = restClientBuilder.clone()
.baseUrl(baseUrl)
.defaultHeaders(authHeaders)
.defaultStatusHandler(responseErrorHandler)
.defaultRequest(requestHeadersSpec -> {
if (!(apiKey instanceof NoopApiKey)) {
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
}
})
.build();
this.webClient = webClientBuilder.clone()
.baseUrl(baseUrl)
.defaultHeaders(authHeaders)
.defaultRequest(requestHeadersSpec -> {
if (!(apiKey instanceof NoopApiKey)) {
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
}
})
.build(); // @formatter:on
this.webClient = webClientBuilder.clone().baseUrl(baseUrl).defaultHeaders(authHeaders).build();
}
public static Builder builder() {

View File

@@ -27,7 +27,6 @@ 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.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
@@ -41,7 +40,6 @@ import org.springframework.web.client.RestClient;
*
* @see <a href= "https://platform.openai.com/docs/api-reference/images">Images</a>
* @author lambochen
* @author Filip Hrisafov
*/
public class OpenAiImageApi {
@@ -64,18 +62,15 @@ public class OpenAiImageApi {
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// @formatter:off
this.restClient = restClientBuilder.clone()
.baseUrl(baseUrl)
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)
.defaultRequest(requestHeadersSpec -> {
if (!(apiKey instanceof NoopApiKey)) {
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
}
})
.build();
// @formatter:on

View File

@@ -27,7 +27,6 @@ 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.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
@@ -41,7 +40,6 @@ import org.springframework.web.client.RestClient;
*
* @author Ahmed Yousri
* @author Ilayaperumal Gopinathan
* @author Filip Hrisafov
* @see <a href=
* "https://platform.openai.com/docs/api-reference/moderations">https://platform.openai.com/docs/api-reference/moderations</a>
*/
@@ -66,20 +64,13 @@ public class OpenAiModerationApi {
this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// @formatter:off
this.restClient = restClientBuilder.clone()
.baseUrl(baseUrl)
.defaultHeaders(h -> {
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
})
.defaultStatusHandler(responseErrorHandler)
.defaultRequest(requestHeadersSpec -> {
if (!(apiKey instanceof NoopApiKey)) {
requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue());
}
})
.build(); // @formatter:on
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) {

View File

@@ -16,27 +16,10 @@
package org.springframework.ai.openai.api;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
@@ -159,126 +142,4 @@ public class OpenAiApiBuilderTests {
.hasMessageContaining("responseErrorHandler cannot be null");
}
@Nested
class MockRequests {
MockWebServer mockWebServer;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void dynamicApiKeyRestClient() throws InterruptedException {
Queue<ApiKey> apiKeys = new LinkedList<>(List.of(new SimpleApiKey("key1"), new SimpleApiKey("key2")));
OpenAiApi api = OpenAiApi.builder()
.apiKey(() -> Objects.requireNonNull(apiKeys.poll()).getValue())
.baseUrl(mockWebServer.url("/").toString())
.build();
MockResponse mockResponse = new MockResponse().setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("""
{
"id": "chatcmpl-12345",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-3.5-turbo",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello world"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
}
""");
mockWebServer.enqueue(mockResponse);
mockWebServer.enqueue(mockResponse);
OpenAiApi.ChatCompletionMessage chatCompletionMessage = new OpenAiApi.ChatCompletionMessage("Hello world",
OpenAiApi.ChatCompletionMessage.Role.USER);
OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(
List.of(chatCompletionMessage), "gpt-3.5-turbo", 0.8, false);
ResponseEntity<OpenAiApi.ChatCompletion> response = api.chatCompletionEntity(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key1");
response = api.chatCompletionEntity(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key2");
}
@Test
void dynamicApiKeyWebClient() throws InterruptedException {
Queue<ApiKey> apiKeys = new LinkedList<>(List.of(new SimpleApiKey("key1"), new SimpleApiKey("key2")));
OpenAiApi api = OpenAiApi.builder()
.apiKey(() -> Objects.requireNonNull(apiKeys.poll()).getValue())
.baseUrl(mockWebServer.url("/").toString())
.build();
MockResponse mockResponse = new MockResponse().setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("""
{
"id": "chatcmpl-12345",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-3.5-turbo",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello world"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15
}
}
""".replace("\n", ""));
mockWebServer.enqueue(mockResponse);
mockWebServer.enqueue(mockResponse);
OpenAiApi.ChatCompletionMessage chatCompletionMessage = new OpenAiApi.ChatCompletionMessage("Hello world",
OpenAiApi.ChatCompletionMessage.Role.USER);
OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(
List.of(chatCompletionMessage), "gpt-3.5-turbo", 0.8, true);
List<OpenAiApi.ChatCompletionChunk> response = api.chatCompletionStream(request).collectList().block();
assertThat(response).hasSize(1);
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key1");
response = api.chatCompletionStream(request).collectList().block();
assertThat(response).hasSize(1);
recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key2");
}
}
}

View File

@@ -1,209 +0,0 @@
/*
* 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.
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
/**
* @author Filip Hrisafov
*/
class OpenAiAudioApiBuilderTests {
private static final ApiKey TEST_API_KEY = new SimpleApiKey("test-api-key");
private static final String TEST_BASE_URL = "https://test.openai.com";
@Test
void testMinimalBuilder() {
OpenAiAudioApi api = OpenAiAudioApi.builder().apiKey(TEST_API_KEY).build();
assertThat(api).isNotNull();
}
@Test
void testFullBuilder() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Custom-Header", "test-value");
RestClient.Builder restClientBuilder = RestClient.builder();
WebClient.Builder webClientBuilder = WebClient.builder();
ResponseErrorHandler errorHandler = mock(ResponseErrorHandler.class);
OpenAiAudioApi api = OpenAiAudioApi.builder()
.baseUrl(TEST_BASE_URL)
.apiKey(TEST_API_KEY)
.headers(headers)
.restClientBuilder(restClientBuilder)
.webClientBuilder(webClientBuilder)
.responseErrorHandler(errorHandler)
.build();
assertThat(api).isNotNull();
}
@Test
void testMissingApiKey() {
assertThatThrownBy(() -> OpenAiAudioApi.builder().build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("apiKey must be set");
}
@Test
void testInvalidBaseUrl() {
assertThatThrownBy(() -> OpenAiAudioApi.builder().baseUrl("").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("baseUrl cannot be null or empty");
assertThatThrownBy(() -> OpenAiAudioApi.builder().baseUrl(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("baseUrl cannot be null or empty");
}
@Test
void testInvalidHeaders() {
assertThatThrownBy(() -> OpenAiAudioApi.builder().headers(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("headers cannot be null");
}
@Test
void testInvalidRestClientBuilder() {
assertThatThrownBy(() -> OpenAiAudioApi.builder().restClientBuilder(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("restClientBuilder cannot be null");
}
@Test
void testInvalidWebClientBuilder() {
assertThatThrownBy(() -> OpenAiAudioApi.builder().webClientBuilder(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("webClientBuilder cannot be null");
}
@Test
void testInvalidResponseErrorHandler() {
assertThatThrownBy(() -> OpenAiAudioApi.builder().responseErrorHandler(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("responseErrorHandler cannot be null");
}
@Nested
class MockRequests {
MockWebServer mockWebServer;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void dynamicApiKeyRestClient() throws InterruptedException {
Queue<ApiKey> apiKeys = new LinkedList<>(List.of(new SimpleApiKey("key1"), new SimpleApiKey("key2")));
OpenAiAudioApi api = OpenAiAudioApi.builder()
.apiKey(() -> Objects.requireNonNull(apiKeys.poll()).getValue())
.baseUrl(mockWebServer.url("/").toString())
.build();
MockResponse mockResponse = new MockResponse().setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
.setBody("Audio bytes as string");
mockWebServer.enqueue(mockResponse);
mockWebServer.enqueue(mockResponse);
OpenAiAudioApi.SpeechRequest request = OpenAiAudioApi.SpeechRequest.builder()
.model(OpenAiAudioApi.TtsModel.TTS_1.value)
.input("Test input")
.build();
ResponseEntity<byte[]> response = api.createSpeech(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key1");
response = api.createSpeech(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key2");
}
@Test
void dynamicApiKeyWebClient() throws InterruptedException {
Queue<ApiKey> apiKeys = new LinkedList<>(List.of(new SimpleApiKey("key1"), new SimpleApiKey("key2")));
OpenAiAudioApi api = OpenAiAudioApi.builder()
.apiKey(() -> Objects.requireNonNull(apiKeys.poll()).getValue())
.baseUrl(mockWebServer.url("/").toString())
.build();
MockResponse mockResponse = new MockResponse().setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
.setBody("Audio bytes as string");
mockWebServer.enqueue(mockResponse);
mockWebServer.enqueue(mockResponse);
OpenAiAudioApi.SpeechRequest request = OpenAiAudioApi.SpeechRequest.builder()
.model(OpenAiAudioApi.TtsModel.TTS_1.value)
.input("Test input")
.build();
List<ResponseEntity<byte[]>> response = api.stream(request).collectList().block();
assertThat(response).hasSize(1);
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key1");
response = api.stream(request).collectList().block();
assertThat(response).hasSize(1);
recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key2");
}
}
}

View File

@@ -1,176 +0,0 @@
/*
* 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.
* 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.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
/**
* @author Filip Hrisafov
*/
class OpenAiImageApiBuilderTests {
private static final ApiKey TEST_API_KEY = new SimpleApiKey("test-api-key");
private static final String TEST_BASE_URL = "https://test.openai.com";
@Test
void testMinimalBuilder() {
OpenAiImageApi api = OpenAiImageApi.builder().apiKey(TEST_API_KEY).build();
assertThat(api).isNotNull();
}
@Test
void testFullBuilder() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Custom-Header", "test-value");
RestClient.Builder restClientBuilder = RestClient.builder();
ResponseErrorHandler errorHandler = mock(ResponseErrorHandler.class);
OpenAiImageApi api = OpenAiImageApi.builder()
.baseUrl(TEST_BASE_URL)
.apiKey(TEST_API_KEY)
.headers(headers)
.restClientBuilder(restClientBuilder)
.responseErrorHandler(errorHandler)
.build();
assertThat(api).isNotNull();
}
@Test
void testMissingApiKey() {
assertThatThrownBy(() -> OpenAiImageApi.builder().build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("apiKey must be set");
}
@Test
void testInvalidBaseUrl() {
assertThatThrownBy(() -> OpenAiImageApi.builder().baseUrl("").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("baseUrl cannot be null or empty");
assertThatThrownBy(() -> OpenAiImageApi.builder().baseUrl(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("baseUrl cannot be null or empty");
}
@Test
void testInvalidHeaders() {
assertThatThrownBy(() -> OpenAiImageApi.builder().headers(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("headers cannot be null");
}
@Test
void testInvalidRestClientBuilder() {
assertThatThrownBy(() -> OpenAiImageApi.builder().restClientBuilder(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("restClientBuilder cannot be null");
}
@Test
void testInvalidResponseErrorHandler() {
assertThatThrownBy(() -> OpenAiImageApi.builder().responseErrorHandler(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("responseErrorHandler cannot be null");
}
@Nested
class MockRequests {
MockWebServer mockWebServer;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void dynamicApiKeyRestClient() throws InterruptedException {
Queue<ApiKey> apiKeys = new LinkedList<>(List.of(new SimpleApiKey("key1"), new SimpleApiKey("key2")));
OpenAiImageApi api = OpenAiImageApi.builder()
.apiKey(() -> Objects.requireNonNull(apiKeys.poll()).getValue())
.baseUrl(mockWebServer.url("/").toString())
.build();
MockResponse mockResponse = new MockResponse().setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("""
{
"created": 1589478378,
"data": [
{
"url": "https://upload.wikimedia.org/wikipedia/commons/4/4e/Mini_Golden_Doodle.jpg"
}
]
}
""");
mockWebServer.enqueue(mockResponse);
mockWebServer.enqueue(mockResponse);
OpenAiImageApi.OpenAiImageRequest request = new OpenAiImageApi.OpenAiImageRequest("Test",
OpenAiImageApi.ImageModel.DALL_E_3.getValue());
ResponseEntity<OpenAiImageApi.OpenAiImageResponse> response = api.createImage(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key1");
response = api.createImage(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key2");
}
}
}

View File

@@ -1,176 +0,0 @@
/*
* 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.
* 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.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiModerationApi;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
/**
* @author Filip Hrisafov
*/
class OpenAiModerationApiBuilderTests {
private static final ApiKey TEST_API_KEY = new SimpleApiKey("test-api-key");
private static final String TEST_BASE_URL = "https://test.openai.com";
@Test
void testMinimalBuilder() {
OpenAiModerationApi api = OpenAiModerationApi.builder().apiKey(TEST_API_KEY).build();
assertThat(api).isNotNull();
}
@Test
void testFullBuilder() {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Custom-Header", "test-value");
RestClient.Builder restClientBuilder = RestClient.builder();
ResponseErrorHandler errorHandler = mock(ResponseErrorHandler.class);
OpenAiModerationApi api = OpenAiModerationApi.builder()
.baseUrl(TEST_BASE_URL)
.apiKey(TEST_API_KEY)
.headers(headers)
.restClientBuilder(restClientBuilder)
.responseErrorHandler(errorHandler)
.build();
assertThat(api).isNotNull();
}
@Test
void testMissingApiKey() {
assertThatThrownBy(() -> OpenAiModerationApi.builder().build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("apiKey must be set");
}
@Test
void testInvalidBaseUrl() {
assertThatThrownBy(() -> OpenAiModerationApi.builder().baseUrl("").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("baseUrl cannot be null or empty");
assertThatThrownBy(() -> OpenAiModerationApi.builder().baseUrl(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("baseUrl cannot be null or empty");
}
@Test
void testInvalidHeaders() {
assertThatThrownBy(() -> OpenAiModerationApi.builder().headers(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("headers cannot be null");
}
@Test
void testInvalidRestClientBuilder() {
assertThatThrownBy(() -> OpenAiModerationApi.builder().restClientBuilder(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("restClientBuilder cannot be null");
}
@Test
void testInvalidResponseErrorHandler() {
assertThatThrownBy(() -> OpenAiModerationApi.builder().responseErrorHandler(null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("responseErrorHandler cannot be null");
}
@Nested
class MockRequests {
MockWebServer mockWebServer;
@BeforeEach
void setUp() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
@Test
void dynamicApiKeyRestClient() throws InterruptedException {
Queue<ApiKey> apiKeys = new LinkedList<>(List.of(new SimpleApiKey("key1"), new SimpleApiKey("key2")));
OpenAiModerationApi api = OpenAiModerationApi.builder()
.apiKey(() -> Objects.requireNonNull(apiKeys.poll()).getValue())
.baseUrl(mockWebServer.url("/").toString())
.build();
MockResponse mockResponse = new MockResponse().setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("""
{
"created": 1589478378,
"data": [
{
"url": "https://upload.wikimedia.org/wikipedia/commons/4/4e/Mini_Golden_Doodle.jpg"
}
]
}
""");
mockWebServer.enqueue(mockResponse);
mockWebServer.enqueue(mockResponse);
OpenAiModerationApi.OpenAiModerationRequest request = new OpenAiModerationApi.OpenAiModerationRequest(
"Test");
ResponseEntity<OpenAiModerationApi.OpenAiModerationResponse> response = api.createModeration(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
RecordedRequest recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key1");
response = api.createModeration(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
recordedRequest = mockWebServer.takeRequest();
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer key2");
}
}
}