Enhance OpenAI Authentication and Configuration

- Add org-id and project-id properties with unified merging logic
- Update autoconfig and docs for all OpenAI models
- Introduce OpenAiChatOptions#httpHeaders option
- Add integration test for httpHeaders and update docs

Resolves #1141
This commit is contained in:
Christian Tzolov
2024-08-01 14:31:21 +02:00
parent 4c1347db17
commit 3978e8ecc2
17 changed files with 863 additions and 733 deletions

View File

@@ -15,7 +15,16 @@
*/
package org.springframework.ai.openai;
import io.micrometer.observation.ObservationRegistry;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
@@ -26,8 +35,16 @@ import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.EmptyUsage;
import org.springframework.ai.chat.metadata.RateLimit;
import org.springframework.ai.chat.model.*;
import org.springframework.ai.chat.observation.*;
import org.springframework.ai.chat.model.AbstractToolCallSupport;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
import org.springframework.ai.chat.observation.ChatModelRequestOptions;
import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
@@ -52,13 +69,13 @@ import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import io.micrometer.observation.ObservationRegistry;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal OpenAI}
* backed by {@link OpenAiApi}.
@@ -204,7 +221,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
.observe(() -> {
ResponseEntity<ChatCompletion> completionEntity = this.retryTemplate
.execute(ctx -> this.openAiApi.chatCompletionEntity(request));
.execute(ctx -> this.openAiApi.chatCompletionEntity(request, getAdditionalHttpHeaders(prompt)));
var chatCompletion = completionEntity.getBody();
@@ -258,7 +275,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
ChatCompletionRequest request = createRequest(prompt, true);
Flux<OpenAiApi.ChatCompletionChunk> completionChunks = this.retryTemplate
.execute(ctx -> this.openAiApi.chatCompletionStream(request));
.execute(ctx -> this.openAiApi.chatCompletionStream(request, getAdditionalHttpHeaders(prompt)));
// For chunked responses, only the first chunk contains the choice role.
// The rest of the chunks with same ID share the same role.
@@ -315,6 +332,16 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
});
}
private MultiValueMap<String, String> getAdditionalHttpHeaders(Prompt prompt) {
Map<String, String> headers = new HashMap<>(this.defaultOptions.getHttpHeaders());
if (prompt.getOptions() != null && prompt.getOptions() instanceof OpenAiChatOptions chatOptions) {
headers.putAll(chatOptions.getHttpHeaders());
}
return CollectionUtils.toMultiValueMap(
headers.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> List.of(e.getValue()))));
}
private Generation buildGeneration(Choice choice, Map<String, Object> metadata) {
List<AssistantMessage.ToolCall> toolCalls = choice.message().toolCalls() == null ? List.of()
: choice.message()

View File

@@ -16,6 +16,7 @@
package org.springframework.ai.openai;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -169,6 +170,13 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
@NestedConfigurationProperty
@JsonIgnore
private Set<String> functions = new HashSet<>();
/**
* Optional HTTP headers to be added to the chat completion request.
*/
@NestedConfigurationProperty
@JsonIgnore
private Map<String, String> httpHeaders = new HashMap<>();
// @formatter:on
public static Builder builder() {
@@ -299,6 +307,12 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
return this;
}
public Builder withHttpHeaders(Map<String, String> httpHeaders) {
Assert.notNull(httpHeaders, "HTTP headers must not be null");
this.options.httpHeaders = httpHeaders;
return this;
}
public OpenAiChatOptions build() {
return this.options;
}
@@ -478,6 +492,14 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.functions = functionNames;
}
public Map<String, String> getHttpHeaders() {
return this.httpHeaders;
}
public void setHttpHeaders(Map<String, String> httpHeaders) {
this.httpHeaders = httpHeaders;
}
@Override
public int hashCode() {
final int prime = 31;
@@ -662,6 +684,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
.withParallelToolCalls(fromOptions.getParallelToolCalls())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.withHttpHeaders(fromOptions.getHttpHeaders())
.build();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.ai.openai.api;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -29,6 +30,7 @@ import org.springframework.http.HttpHeaders;
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;
@@ -81,21 +83,48 @@ public class OpenAiAudioApi {
/**
* Create an new chat completion api.
* @param baseUrl api base URL.
* @param openAiToken OpenAI apiKey.
* @param apiKey OpenAI apiKey.
* @param restClientBuilder RestClient builder.
* @param webClientBuilder WebClient builder.
* @param responseErrorHandler Response error handler.
*/
public OpenAiAudioApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder,
public OpenAiAudioApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) {
this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(headers -> {
headers.setBearerAuth(openAiToken);
}).defaultStatusHandler(responseErrorHandler).build();
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, webClientBuilder,
responseErrorHandler);
}
this.webClient = webClientBuilder.baseUrl(baseUrl).defaultHeaders(headers -> {
headers.setBearerAuth(openAiToken);
}).defaultHeaders(ApiUtils.getJsonContentHeaders(openAiToken)).build();
/**
* Create an new chat completion 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, String apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler) {
// @formatter:off
this.restClient = restClientBuilder
.baseUrl(baseUrl)
.defaultHeaders(h -> {
h.setBearerAuth(apiKey);
h.addAll(headers);
})
.defaultStatusHandler(responseErrorHandler).build();
this.webClient = webClientBuilder
.baseUrl(baseUrl)
.defaultHeaders(h -> {
h.setBearerAuth(apiKey);
h.addAll(headers);
})
.defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)).build();
// @formatter:on
}
/**

View File

@@ -16,18 +16,21 @@
package org.springframework.ai.openai.api;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.util.api.ApiUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* OpenAI Image API.
*
@@ -60,17 +63,36 @@ 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 apiKey OpenAI apiKey.
* @param restClientBuilder the rest client builder to use.
* @param responseErrorHandler the response error handler to use.
*/
public OpenAiImageApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder,
public OpenAiImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), 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, String apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// @formatter:off
this.restClient = restClientBuilder.baseUrl(baseUrl)
.defaultHeaders(ApiUtils.getJsonContentHeaders(openAiToken))
.defaultHeaders(h -> {
h.setBearerAuth(apiKey);
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
})
.defaultStatusHandler(responseErrorHandler)
.build();
// @formatter:on
}
/**

View File

@@ -42,6 +42,7 @@ import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionChunk;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
@@ -58,7 +59,10 @@ public class MessageTypeContentTests {
OpenAiChatModel chatModel;
@Captor
ArgumentCaptor<ChatCompletionRequest> promptCaptor;
ArgumentCaptor<ChatCompletionRequest> pomptCaptor;
@Captor
ArgumentCaptor<MultiValueMap<String, String>> headersCaptor;
Flux<ChatCompletionChunk> fluxResponse = Flux
.generate(() -> new ChatCompletionChunk("id", List.of(), 0l, "model", "fp", "object", null), (state, sink) -> {
@@ -75,31 +79,35 @@ public class MessageTypeContentTests {
@Test
public void systemMessageSimpleContentType() {
when(openAiApi.chatCompletionEntity(promptCaptor.capture())).thenReturn(Mockito.mock(ResponseEntity.class));
when(openAiApi.chatCompletionEntity(pomptCaptor.capture(), headersCaptor.capture()))
.thenReturn(Mockito.mock(ResponseEntity.class));
chatModel.call(new Prompt(List.of(new SystemMessage("test message"))));
validateStringContent(promptCaptor.getValue());
validateStringContent(pomptCaptor.getValue());
assertThat(headersCaptor.getValue()).isEmpty();
}
@Test
public void userMessageSimpleContentType() {
when(openAiApi.chatCompletionEntity(promptCaptor.capture())).thenReturn(Mockito.mock(ResponseEntity.class));
when(openAiApi.chatCompletionEntity(pomptCaptor.capture(), headersCaptor.capture()))
.thenReturn(Mockito.mock(ResponseEntity.class));
chatModel.call(new Prompt(List.of(new UserMessage("test message"))));
validateStringContent(promptCaptor.getValue());
validateStringContent(pomptCaptor.getValue());
}
@Test
public void streamUserMessageSimpleContentType() {
when(openAiApi.chatCompletionStream(promptCaptor.capture())).thenReturn(fluxResponse);
when(openAiApi.chatCompletionStream(pomptCaptor.capture(), headersCaptor.capture())).thenReturn(fluxResponse);
chatModel.stream(new Prompt(List.of(new UserMessage("test message"))));
validateStringContent(promptCaptor.getValue());
validateStringContent(pomptCaptor.getValue());
assertThat(headersCaptor.getValue()).isEmpty();
}
private void validateStringContent(ChatCompletionRequest chatCompletionRequest) {
@@ -113,25 +121,26 @@ public class MessageTypeContentTests {
@Test
public void userMessageWithMediaType() throws MalformedURLException {
when(openAiApi.chatCompletionEntity(promptCaptor.capture())).thenReturn(Mockito.mock(ResponseEntity.class));
when(openAiApi.chatCompletionEntity(pomptCaptor.capture(), headersCaptor.capture()))
.thenReturn(Mockito.mock(ResponseEntity.class));
URL mediaUrl = new URL("http://test");
chatModel.call(new Prompt(
List.of(new UserMessage("test message", List.of(new Media(MimeTypeUtils.IMAGE_JPEG, mediaUrl))))));
validateComplexContent(promptCaptor.getValue());
validateComplexContent(pomptCaptor.getValue());
}
@Test
public void streamUserMessageWithMediaType() throws MalformedURLException {
when(openAiApi.chatCompletionStream(promptCaptor.capture())).thenReturn(fluxResponse);
when(openAiApi.chatCompletionStream(pomptCaptor.capture(), headersCaptor.capture())).thenReturn(fluxResponse);
URL mediaUrl = new URL("http://test");
chatModel.stream(new Prompt(
List.of(new UserMessage("test message", List.of(new Media(MimeTypeUtils.IMAGE_JPEG, mediaUrl))))));
validateComplexContent(promptCaptor.getValue());
validateComplexContent(pomptCaptor.getValue());
}
private void validateComplexContent(ChatCompletionRequest chatCompletionRequest) {

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2024 - 2024 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 static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
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.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;
/**
* @author Christian Tzolov
*/
@SpringBootTest(classes = OpenAiChatModeAdditionalHttpHeadersIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiChatModeAdditionalHttpHeadersIT {
@Autowired
private OpenAiChatModel openAiChatModel;
@Test
void additionalApiKeyHeader() {
assertThrows(NonTransientAiException.class, () -> {
this.openAiChatModel.call("Tell me a joke");
});
// Use the additional headers to override the Api Key.
// Mind that you have to prefix the Api Key with the "Bearer " prefix.
OpenAiChatOptions options = OpenAiChatOptions.builder()
.withHttpHeaders(Map.of("Authorization", "Bearer " + System.getenv("OPENAI_API_KEY")))
.build();
ChatResponse response = this.openAiChatModel.call(new Prompt("Tell me a joke", options));
assertThat(response).isNotNull();
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi("Invalid API Key");
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
}
}

View File

@@ -69,6 +69,7 @@ import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
@@ -141,7 +142,7 @@ public class OpenAiRetryTests {
ChatCompletion expectedChatCompletion = new ChatCompletion("id", List.of(choice), 666l, "model", null, null,
new OpenAiApi.Usage(10, 10, 10));
when(openAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
when(openAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class), any()))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
@@ -156,8 +157,8 @@ public class OpenAiRetryTests {
@Test
public void openAiChatNonTransientError() {
when(openAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
when(openAiApi.chatCompletionEntity(isA(ChatCompletionRequest.class), any()))
.thenThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> chatModel.call(new Prompt("text")));
}
@@ -169,7 +170,7 @@ public class OpenAiRetryTests {
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", List.of(choice), 666l, "model", null,
null, null);
when(openAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
when(openAiApi.chatCompletionStream(isA(ChatCompletionRequest.class), any()))
.thenThrow(new TransientAiException("Transient Error 1"))
.thenThrow(new TransientAiException("Transient Error 2"))
.thenReturn(Flux.just(expectedChatCompletion));
@@ -184,8 +185,8 @@ public class OpenAiRetryTests {
@Test
public void openAiChatStreamNonTransientError() {
when(openAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
when(openAiApi.chatCompletionStream(isA(ChatCompletionRequest.class), any()))
.thenThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> chatModel.stream(new Prompt("text")));
}
@@ -210,10 +211,9 @@ public class OpenAiRetryTests {
@Test
public void openAiEmbeddingNonTransientError() {
when(openAiApi.embeddings(isA(EmbeddingRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
when(openAiApi.embeddings(isA(EmbeddingRequest.class))).thenThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> embeddingModel
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
}
@Test
@@ -238,9 +238,9 @@ public class OpenAiRetryTests {
@Test
public void openAiAudioTranscriptionNonTransientError() {
when(openAiAudioApi.createTranscription(isA(TranscriptionRequest.class), isA(Class.class)))
.thenThrow(new RuntimeException("Transient Error 1"));
.thenThrow(new RuntimeException("Transient Error 1"));
assertThrows(RuntimeException.class, () -> audioTranscriptionModel
.call(new AudioTranscriptionPrompt(new ClassPathResource("speech/jfk.flac"))));
.call(new AudioTranscriptionPrompt(new ClassPathResource("speech/jfk.flac"))));
}
@Test
@@ -264,7 +264,7 @@ public class OpenAiRetryTests {
@Test
public void openAiImageNonTransientError() {
when(openAiImageApi.createImage(isA(OpenAiImageRequest.class)))
.thenThrow(new RuntimeException("Transient Error 1"));
.thenThrow(new RuntimeException("Transient Error 1"));
assertThrows(RuntimeException.class,
() -> imageModel.call(new ImagePrompt(List.of(new ImageMessage("Image Message")))));
}

View File

@@ -1,4 +1,4 @@
= OpenAI Text-to-Speech (TTS) Integration
= OpenAI Text-to-Speech (TTS)
== Introduction
@@ -37,7 +37,25 @@ dependencies {
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== TTS Properties
== Speech Properties
=== Connection Properties
The prefix `spring.ai.openai` is used as the property prefix that lets you connect to OpenAI.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.project-id | Optionally, you can specify which project is used for an API request. | -
|====
TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), optionally, you can specify which organization and project is used for an API request.
Usage from these API requests will count as usage for the specified organization and project.
=== Configuraiton Properties
The prefix `spring.ai.openai.audio.speech` is used as the property prefix that lets you configure the OpenAI Text-to-Speech client.
@@ -45,12 +63,22 @@ The prefix `spring.ai.openai.audio.speech` is used as the property prefix that l
|====
| Property | Description | Default
| spring.ai.openai.audio.speech.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.audio.speech.api-key | The API Key | -
| spring.ai.openai.audio.speech.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.audio.speech.project-id | Optionally, you can specify which project is used for an API request. | -
| spring.ai.openai.audio.speech.options.model | ID of the model to use. Only tts-1 is currently available. | tts-1
| spring.ai.openai.audio.speech.options.voice | The voice to use for the TTS output. Available options are: alloy, echo, fable, onyx, nova, and shimmer. | alloy
| spring.ai.openai.audio.speech.options.response-format | The format of the audio output. Supported formats are mp3, opus, aac, flac, wav, and pcm. | mp3
| spring.ai.openai.audio.speech.options.speed | The speed of the voice synthesis. The acceptable range is from 0.0 (slowest) to 1.0 (fastest). | 1.0
|====
NOTE: You can override the common `spring.ai.openai.base-url`, `spring.ai.openai.api-key`, `spring.ai.openai.organization-id` and `spring.ai.openai.project-id` properties.
The `spring.ai.openai.audio.speech.base-url`, `spring.ai.openai.audio.speech.api-key`, `spring.ai.openai.audio.speech.organization-id` and `spring.ai.openai.audio.speech.project-id` properties if set take precedence over the common properties.
This is useful if you want to use different OpenAI accounts for different models and different model endpoints.
TIP: All properties prefixed with `spring.ai.openai.image.options` can be overridden at runtime.
== Runtime Options [[speech-options]]
The `OpenAiAudioSpeechOptions` class provides the options to use when making a text-to-speech request.

View File

@@ -1,20 +1,22 @@
= OpenAI Transcriptions
== OpenAI Transcriptions
Spring AI supports https://platform.openai.com/docs/api-reference/audio/createTranscription[OpenAI's Transcription model].
== Prerequisites
You will need to create an API key with OpenAI to access ChatGPT models.
Create an account at https://platform.openai.com/signup[OpenAI signup page] and generate the token on the https://platform.openai.com/account/api-keys[API Keys page].
The Spring AI project defines a configuration property named `spring.ai.openai.api-key` that you should set to the value of the `API Key` obtained from openai.com.
Exporting an environment variable is one way to set that configuration property:
== Auto-configuration
Spring AI provides Spring Boot auto-configuration for the OpenAI Image Generation Client.
To enable it, add the following dependency to your project's Maven `pom.xml` file:
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source,xml]
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
@@ -35,12 +37,34 @@ TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Man
=== Transcription Properties
==== Connection Properties
The prefix `spring.ai.openai` is used as the property prefix that lets you connect to OpenAI.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.project-id | Optionally, you can specify which project is used for an API request. | -
|====
TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), optionally, you can specify which organization and project is used for an API request.
Usage from these API requests will count as usage for the specified organization and project.
==== Configuraiton Properties
The prefix `spring.ai.openai.audio.transcription` is used as the property prefix that lets you configure the retry mechanism for the OpenAI image model.
[cols="3,5,2"]
|====
| Property | Description | Default
| spring.ai.openai.audio.transcription.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.audio.transcription.api-key | The API Key | -
| spring.ai.openai.audio.transcription.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.audio.transcription.project-id | Optionally, you can specify which project is used for an API request. | -
| spring.ai.openai.audio.transcription.options.model | ID of the model to use. Only whisper-1 (which is powered by our open source Whisper V2 model) is currently available. | whisper-1
| spring.ai.openai.audio.transcription.options.response-format | The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. | json
| spring.ai.openai.audio.transcription.options.prompt | An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. |
@@ -49,10 +73,16 @@ The prefix `spring.ai.openai.audio.transcription` is used as the property prefix
| spring.ai.openai.audio.transcription.options.timestamp_granularities | The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. | segment
|====
NOTE: You can override the common `spring.ai.openai.base-url`, `spring.ai.openai.api-key`, `spring.ai.openai.organization-id` and `spring.ai.openai.project-id` properties.
The `spring.ai.openai.audio.transcription.base-url`, `spring.ai.openai.audio.transcription.api-key`, `spring.ai.openai.audio.transcription.organization-id` and `spring.ai.openai.audio.transcription.project-id` properties if set take precedence over the common properties.
This is useful if you want to use different OpenAI accounts for different models and different model endpoints.
TIP: All properties prefixed with `spring.ai.openai.image.options` can be overridden at runtime.
== Runtime Options [[image-options]]
The `OpenAiAudioTranscriptionOptions` class provides the options to use when making a transcription.
On start-up, the options specified by `spring.ai.openai.audio.transcription` are used, but you can override these at runtime.
On start-up, the options specified by `spring.ai.openai.audio.transcription` are used but you can override these at runtime.
For example:
@@ -74,7 +104,7 @@ AudioTranscriptionResponse response = openAiTranscriptionModel.call(transcriptio
Add the `spring-ai-openai` dependency to your project's Maven `pom.xml` file:
[source,xml]
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
@@ -113,5 +143,4 @@ AudioTranscriptionResponse response = openAiTranscriptionModel.call(transcriptio
----
== Example Code
* The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/transcription/OpenAiTranscriptionModelIT.java[OpenAiTranscriptionModelIT.java] test provides some general examples how to use the library.
* The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/transcription/OpenAiTranscriptionModelIT.java[OpenAiTranscriptionModelIT.java] test provides some general examples how to use the library.

View File

@@ -74,10 +74,14 @@ The prefix `spring.ai.openai` is used as the property prefix that lets you conne
|====
| Property | Description | Default
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.project-id | Optionally, you can specify which project is used for an API request. | -
|====
TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), optionally, you can specify which organization and project is used for an API request.
Usage from these API requests will count as usage for the specified organization and project.
==== Configuration Properties
@@ -91,6 +95,8 @@ The prefix `spring.ai.openai.chat` is the property prefix that lets you configur
| spring.ai.openai.chat.base-url | Optional overrides the spring.ai.openai.base-url to provide chat specific url | -
| spring.ai.openai.chat.completions-path | The path to append to the base-url | `/v1/chat/completions`
| spring.ai.openai.chat.api-key | Optional overrides the spring.ai.openai.api-key to provide chat specific api-key | -
| spring.ai.openai.chat.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.chat.project-id | Optionally, you can specify which project is used for an API request. | -
| spring.ai.openai.chat.options.model | This is the OpenAI Chat model to use. `gpt-4o`, `gpt-4-turbo`, `gpt-4-turbo-2024-04-09`, `gpt-4-0125-preview`, `gpt-4-turbo-preview`, `gpt-3.5-turbo`, `gpt-3.5-turbo-0125`, `gpt-3.5-turbo-1106`. See the https://platform.openai.com/docs/models[models] page for more information. | `gpt-3.5-turbo`
| spring.ai.openai.chat.options.temperature | The sampling temperature to use that controls the apparent creativity of generated completions. Higher values will make output more random while lower values will make results more focused and deterministic. It is not recommended to modify temperature and top_p for the same completions request as the interaction of these two settings is difficult to predict. | 0.8
| spring.ai.openai.chat.options.frequencyPenalty | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. | 0.0f
@@ -108,6 +114,7 @@ The prefix `spring.ai.openai.chat` is the property prefix that lets you configur
| spring.ai.openai.chat.options.functions | List of functions, identified by their names, to enable for function calling in a single prompt requests. Functions with those names must exist in the functionCallbacks registry. | -
| spring.ai.openai.chat.options.stream-usage | (For streaming only) Set to add an additional chunk with token usage statistics for the entire request. The `choices` field for this chunk is an empty array and all other chunks will also include a usage field, but with a null value. | false
| spring.ai.openai.chat.options.parallel-tool-calls | Whether to enable link:https://platform.openai.com/docs/guides/function-calling/parallel-function-calling[parallel function calling] during tool use. | true
| spring.ai.openai.chat.options.http-headers | Optional HTTP headers to be added to the chat completion request. To override the api-key you need to use a `Authorization` header key and you have to prefix the key value with the `Bearer ` prefix. | -
|====
NOTE: You can override the common `spring.ai.openai.base-url` and `spring.ai.openai.api-key` for the `ChatModel` and `EmbeddingModel` implementations.

View File

@@ -77,8 +77,13 @@ The prefix `spring.ai.openai` is used as the property prefix that lets you conne
| spring.ai.openai.base-url | The URL to connect to | +https://api.openai.com+
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.project-id | Optionally, you can specify which project is used for an API request. | -
|====
TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), optionally, you can specify which organization and project is used for an API request.
Usage from these API requests will count as usage for the specified organization and project.
==== Configuration Properties
The prefix `spring.ai.openai.embedding` is property prefix that configures the `EmbeddingModel` implementation for OpenAI.
@@ -91,6 +96,8 @@ The prefix `spring.ai.openai.embedding` is property prefix that configures the `
| spring.ai.openai.embedding.base-url | Optional overrides the spring.ai.openai.base-url to provide embedding specific url | -
| spring.ai.openai.chat.embeddings-path | The path to append to the base-url | `/v1/embeddings`
| spring.ai.openai.embedding.api-key | Optional overrides the spring.ai.openai.api-key to provide embedding specific api-key | -
| spring.ai.openai.embedding.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.embedding.project-id | Optionally, you can specify which project is used for an API request. | -
| spring.ai.openai.embedding.metadata-mode | Document content extraction mode. | EMBED
| spring.ai.openai.embedding.options.model | The model to use | text-embedding-ada-002 (other options: text-embedding-3-large, text-embedding-3-small)
| spring.ai.openai.embedding.options.encodingFormat | The format to return the embeddings in. Can be either float or base64. | -

View File

@@ -41,26 +41,6 @@ TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Man
=== Image Generation Properties
The prefix `spring.ai.openai.image` is the property prefix that lets you configure the `ImageModel` implementation for OpenAI.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.openai.image.enabled | Enable OpenAI image model. | true
| spring.ai.openai.image.base-url | Optional overrides the spring.ai.openai.base-url to provide chat specific url | -
| spring.ai.openai.image.api-key | Optional overrides the spring.ai.openai.api-key to provide chat specific api-key | -
| spring.ai.openai.image.options.n | The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported. | -
| spring.ai.openai.image.options.model | The model to use for image generation. | OpenAiImageApi.DEFAULT_IMAGE_MODEL
| spring.ai.openai.image.options.quality | The quality of the image that will be generated. HD creates images with finer details and greater consistency across the image. This parameter is only supported for dall-e-3. | -
| spring.ai.openai.image.options.response_format | The format in which the generated images are returned. Must be one of URL or b64_json. | -
| `spring.ai.openai.image.options.size` | The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. | -
| `spring.ai.openai.image.options.size_width` | The width of the generated images. Must be one of 256, 512, or 1024 for dall-e-2. | -
| `spring.ai.openai.image.options.size_height`| The height of the generated images. Must be one of 256, 512, or 1024 for dall-e-2. | -
| `spring.ai.openai.image.options.style` | The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This parameter is only supported for dall-e-3. | -
| `spring.ai.openai.image.options.user` | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. | -
|====
==== Connection Properties
The prefix `spring.ai.openai` is used as the property prefix that lets you connect to OpenAI.
@@ -70,9 +50,12 @@ The prefix `spring.ai.openai` is used as the property prefix that lets you conne
| Property | Description | Default
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.project-id | Optionally, you can specify which project is used for an API request. | -
|====
==== Configuration Properties
TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), optionally, you can specify which organization and project is used for an API request.
Usage from these API requests will count as usage for the specified organization and project.
==== Retry Properties
@@ -92,6 +75,34 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config
| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty
|====
==== Configuration Properties
The prefix `spring.ai.openai.image` is the property prefix that lets you configure the `ImageModel` implementation for OpenAI.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.openai.image.enabled | Enable OpenAI image model. | true
| spring.ai.openai.image.base-url | Optional overrides the spring.ai.openai.base-url to provide chat specific url | -
| spring.ai.openai.image.api-key | Optional overrides the spring.ai.openai.api-key to provide chat specific api-key | -
| spring.ai.openai.image.organization-id | Optionally you can specify which organization used for an API request. | -
| spring.ai.openai.image.project-id | Optionally, you can specify which project is used for an API request. | -
| spring.ai.openai.image.options.n | The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported. | -
| spring.ai.openai.image.options.model | The model to use for image generation. | OpenAiImageApi.DEFAULT_IMAGE_MODEL
| spring.ai.openai.image.options.quality | The quality of the image that will be generated. HD creates images with finer details and greater consistency across the image. This parameter is only supported for dall-e-3. | -
| spring.ai.openai.image.options.response_format | The format in which the generated images are returned. Must be one of URL or b64_json. | -
| `spring.ai.openai.image.options.size` | The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. | -
| `spring.ai.openai.image.options.size_width` | The width of the generated images. Must be one of 256, 512, or 1024 for dall-e-2. | -
| `spring.ai.openai.image.options.size_height`| The height of the generated images. Must be one of 256, 512, or 1024 for dall-e-2. | -
| `spring.ai.openai.image.options.style` | The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This parameter is only supported for dall-e-3. | -
| `spring.ai.openai.image.options.user` | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. | -
|====
NOTE: You can override the common `spring.ai.openai.base-url`, `spring.ai.openai.api-key`, `spring.ai.openai.organization-id` and `spring.ai.openai.project-id` properties.
The `spring.ai.openai.image.base-url`, `spring.ai.openai.image.api-key`, `spring.ai.openai.image.organization-id` and `spring.ai.openai.image.project-id` properties if set take precedence over the common properties.
This is useful if you want to use different OpenAI accounts for different models and different model endpoints.
TIP: All properties prefixed with `spring.ai.openai.image.options` can be overridden at runtime.
== Runtime Options [[image-options]]

View File

@@ -1,144 +0,0 @@
= OpenAI Text-to-Speech (TTS) Integration
== Introduction
The Audio API provides a speech endpoint based on OpenAI's TTS (text-to-speech) model, enabling users to:
- Narrate a written blog post.
- Produce spoken audio in multiple languages.
- Give real-time audio output using streaming.
== Prerequisites
. Create an OpenAI account and obtain an API key. You can sign up at the https://platform.openai.com/signup[OpenAI signup page] and generate an API key on the https://platform.openai.com/account/api-keys[API Keys page].
. Add the `spring-ai-openai` dependency to your project's build file. For more information, refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section.
== Auto-configuration
Spring AI provides Spring Boot auto-configuration for the OpenAI Text-to-Speech Client.
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source,xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file:
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== TTS Properties
The prefix `spring.ai.openai.audio.speech` is used as the property prefix that lets you configure the OpenAI Text-to-Speech client.
[cols="3,5,2"]
|====
| Property | Description | Default
| spring.ai.openai.audio.speech.options.model | ID of the model to use. Only tts-1 is currently available. | tts-1
| spring.ai.openai.audio.speech.options.voice | The voice to use for the TTS output. Available options are: alloy, echo, fable, onyx, nova, and shimmer. | alloy
| spring.ai.openai.audio.speech.options.response-format | The format of the audio output. Supported formats are mp3, opus, aac, flac, wav, and pcm. | mp3
| spring.ai.openai.audio.speech.options.speed | The speed of the voice synthesis. The acceptable range is from 0.0 (slowest) to 1.0 (fastest). | 1.0
|====
== Runtime Options [[speech-options]]
The `OpenAiAudioSpeechOptions` class provides the options to use when making a text-to-speech request.
On start-up, the options specified by `spring.ai.openai.audio.speech` are used but you can override these at runtime.
For example:
[source,java]
----
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
.withModel("tts-1")
.withVoice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
.withResponseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.withSpeed(1.0f)
.build();
SpeechPrompt speechPrompt = new SpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
SpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);
----
== Manual Configuration
Add the `spring-ai-openai` dependency to your project's Maven `pom.xml` file:
[source,xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file:
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-openai'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
Next, create an `OpenAiAudioSpeechModel`:
[source,java]
----
var openAiAudioApi = new OpenAiAudioApi(System.getenv("OPENAI_API_KEY"));
var openAiAudioSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi);
var speechOptions = OpenAiAudioSpeechOptions.builder()
.withResponseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.withSpeed(1.0f)
.withModel(OpenAiAudioApi.TtsModel.TTS_1.value)
.build();
var speechPrompt = new SpeechPrompt("Hello, this is a text-to-speech example.", speechOptions);
SpeechResponse response = openAiAudioSpeechModel.call(speechPrompt);
// Accessing metadata (rate limit info)
OpenAiAudioSpeechResponseMetadata metadata = response.getMetadata();
byte[] responseAsBytes = response.getResult().getOutput();
----
== Streaming Real-time Audio
The Speech API provides support for real-time audio streaming using chunk transfer encoding. This means that the audio is able to be played before the full file has been generated and made accessible.
[source,java]
----
var openAiAudioApi = new OpenAiAudioApi(System.getenv("OPENAI_API_KEY"));
var openAiAudioSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi);
OpenAiAudioSpeechOptions speechOptions = OpenAiAudioSpeechOptions.builder()
.withVoice(OpenAiAudioApi.SpeechRequest.Voice.ALLOY)
.withSpeed(1.0f)
.withResponseFormat(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3)
.withModel(OpenAiAudioApi.TtsModel.TTS_1.value)
.build();
SpeechPrompt speechPrompt = new SpeechPrompt("Today is a wonderful day to build something people love!", speechOptions);
Flux<SpeechResponse> responseStream = openAiAudioSpeechModel.stream(speechPrompt);
----
== Example Code
* The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/speech/OpenAiSpeechModelIT.java[OpenAiSpeechModelIT.java] test provides some general examples of how to use the library.

View File

@@ -1,118 +0,0 @@
== OpenAI Transcriptions
Spring AI supports https://platform.openai.com/docs/api-reference/audio/createTranscription[OpenAI's Transcription model].
== Prerequisites
You will need to create an API key with OpenAI to access ChatGPT models.
Create an account at https://platform.openai.com/signup[OpenAI signup page] and generate the token on the https://platform.openai.com/account/api-keys[API Keys page].
The Spring AI project defines a configuration property named `spring.ai.openai.api-key` that you should set to the value of the `API Key` obtained from openai.com.
Exporting an environment variable is one way to set that configuration property:
== Auto-configuration
Spring AI provides Spring Boot auto-configuration for the OpenAI Image Generation Client.
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== Transcription Properties
The prefix `spring.ai.openai.audio.transcription` is used as the property prefix that lets you configure the retry mechanism for the OpenAI image model.
[cols="3,5,2"]
|====
| Property | Description | Default
| spring.ai.openai.audio.transcription.options.model | ID of the model to use. Only whisper-1 (which is powered by our open source Whisper V2 model) is currently available. | whisper-1
| spring.ai.openai.audio.transcription.options.response-format | The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. | json
| spring.ai.openai.audio.transcription.options.prompt | An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. |
| spring.ai.openai.audio.transcription.options.language | The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency. |
| spring.ai.openai.audio.transcription.options.temperature | The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. | 0
| spring.ai.openai.audio.transcription.options.timestamp_granularities | The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. Note: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. | segment
|====
== Runtime Options [[image-options]]
The `OpenAiAudioTranscriptionOptions` class provides the options to use when making a transcription.
On start-up, the options specified by `spring.ai.openai.audio.transcription` are used but you can override these at runtime.
For example:
[source,java]
----
OpenAiAudioApi.TranscriptResponseFormat responseFormat = OpenAiAudioApi.TranscriptResponseFormat.VTT;
OpenAiAudioTranscriptionOptions transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
.withLanguage("en")
.withPrompt("Ask not this, but ask that")
.withTemperature(0f)
.withResponseFormat(responseFormat)
.build();
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioFile, transcriptionOptions);
AudioTranscriptionResponse response = openAiTranscriptionModel.call(transcriptionRequest);
----
== Manual Configuration
Add the `spring-ai-openai` dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-openai'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
Next, create a `OpenAiAudioTranscriptionModel`
[source,java]
----
var openAiAudioApi = new OpenAiAudioApi(System.getenv("OPENAI_API_KEY"));
var openAiAudioTranscriptionModel = new OpenAiAudioTranscriptionModel(openAiAudioApi);
var transcriptionOptions = OpenAiAudioTranscriptionOptions.builder()
.withResponseFormat(TranscriptResponseFormat.TEXT)
.withTemperature(0f)
.build();
var audioFile = new FileSystemResource("/path/to/your/resource/speech/jfk.flac");
AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioFile, transcriptionOptions);
AudioTranscriptionResponse response = openAiTranscriptionModel.call(transcriptionRequest);
----
== Example Code
* The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/transcription/OpenAiTranscriptionModelIT.java[OpenAiTranscriptionModelIT.java] test provides some general examples how to use the library.

View File

@@ -15,14 +15,22 @@
*/
package org.springframework.ai.autoconfigure.openai;
import io.micrometer.observation.ObservationRegistry;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.openai.*;
import org.springframework.ai.openai.OpenAiAudioSpeechModel;
import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.ai.openai.api.OpenAiImageApi;
@@ -37,15 +45,16 @@ import org.springframework.boot.autoconfigure.web.reactive.function.client.WebCl
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.lang.NonNull;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
/**
* @author Christian Tzolov
@@ -109,45 +118,27 @@ public class OpenAiAutoConfiguration {
private OpenAiApi openAiApi(OpenAiChatProperties chatProperties, OpenAiConnectionProperties commonProperties,
RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler, String modelType) {
ResolvedBaseUrlAndApiKey result = getResolvedBaseUrlAndApiKey(chatProperties.getBaseUrl(),
chatProperties.getApiKey(), commonProperties, modelType);
return new OpenAiApi(result.resolvedBaseUrl(), result.resolvedApiKey(), chatProperties.getCompletionsPath(),
OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH, restClientBuilder, webClientBuilder,
responseErrorHandler);
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties,
modelType);
return new OpenAiApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
chatProperties.getCompletionsPath(), OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH,
restClientBuilder, webClientBuilder, responseErrorHandler);
}
private OpenAiApi openAiApi(OpenAiEmbeddingProperties embeddingProperties,
OpenAiConnectionProperties commonProperties, RestClient.Builder restClientBuilder,
WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler, String modelType) {
ResolvedBaseUrlAndApiKey result = getResolvedBaseUrlAndApiKey(embeddingProperties.getBaseUrl(),
embeddingProperties.getApiKey(), commonProperties, modelType);
return new OpenAiApi(result.resolvedBaseUrl(), result.resolvedApiKey(),
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties,
modelType);
return new OpenAiApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH, embeddingProperties.getEmbeddingsPath(),
restClientBuilder, webClientBuilder, responseErrorHandler);
}
private static @NonNull ResolvedBaseUrlAndApiKey getResolvedBaseUrlAndApiKey(String baseUrl, String apiKey,
OpenAiConnectionProperties commonProperties, String modelType) {
var commonBaseUrl = commonProperties.getBaseUrl();
var commonApiKey = commonProperties.getApiKey();
String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
Assert.hasText(resolvedBaseUrl,
"OpenAI base URL must be set. Use the connection property: spring.ai.openai.base-url or spring.ai.openai."
+ modelType + ".base-url property.");
String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
Assert.hasText(resolvedApiKey,
"OpenAI API key must be set. Use the connection property: spring.ai.openai.api-key or spring.ai.openai."
+ modelType + ".api-key property.");
return new ResolvedBaseUrlAndApiKey(resolvedBaseUrl, resolvedApiKey);
}
private record ResolvedBaseUrlAndApiKey(String resolvedBaseUrl, String resolvedApiKey) {
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = OpenAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
@@ -157,18 +148,10 @@ public class OpenAiAutoConfiguration {
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ImageModelObservationConvention> observationConvention) {
String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey()
: commonProperties.getApiKey();
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image");
String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl()
: commonProperties.getBaseUrl();
Assert.hasText(apiKey,
"OpenAI API key must be set. Use the property: spring.ai.openai.api-key or spring.ai.openai.image.api-key property.");
Assert.hasText(baseUrl,
"OpenAI base URL must be set. Use the property: spring.ai.openai.base-url or spring.ai.openai.image.base-url property.");
var openAiImageApi = new OpenAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler);
var openAiImageApi = new OpenAiImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
restClientBuilder, responseErrorHandler);
var imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
@@ -187,19 +170,11 @@ public class OpenAiAutoConfiguration {
RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler) {
String apiKey = StringUtils.hasText(transcriptionProperties.getApiKey()) ? transcriptionProperties.getApiKey()
: commonProperties.getApiKey();
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, transcriptionProperties,
"transcription");
String baseUrl = StringUtils.hasText(transcriptionProperties.getBaseUrl())
? transcriptionProperties.getBaseUrl() : commonProperties.getBaseUrl();
Assert.hasText(apiKey,
"OpenAI API key must be set. Use the property: spring.ai.openai.api-key or spring.ai.openai.audio.transcription.api-key property.");
Assert.hasText(baseUrl,
"OpenAI base URL must be set. Use the property: spring.ai.openai.base-url or spring.ai.openai.audio.transcription.base-url property.");
var openAiAudioApi = new OpenAiAudioApi(baseUrl, apiKey, restClientBuilder, webClientBuilder,
responseErrorHandler);
var openAiAudioApi = new OpenAiAudioApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
restClientBuilder, webClientBuilder, responseErrorHandler);
return new OpenAiAudioTranscriptionModel(openAiAudioApi, transcriptionProperties.getOptions(), retryTemplate);
@@ -214,19 +189,11 @@ public class OpenAiAutoConfiguration {
RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
ResponseErrorHandler responseErrorHandler) {
String apiKey = StringUtils.hasText(speechProperties.getApiKey()) ? speechProperties.getApiKey()
: commonProperties.getApiKey();
ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechProperties,
"speach");
String baseUrl = StringUtils.hasText(speechProperties.getBaseUrl()) ? speechProperties.getBaseUrl()
: commonProperties.getBaseUrl();
Assert.hasText(apiKey,
"OpenAI API key must be set. Use the property: spring.ai.openai.api-key or spring.ai.openai.audio.speech.api-key property.");
Assert.hasText(baseUrl,
"OpenAI base URL must be set. Use the property: spring.ai.openai.base-url or spring.ai.openai.audio.speech.base-url property.");
var openAiAudioApi = new OpenAiAudioApi(baseUrl, apiKey, restClientBuilder, webClientBuilder,
responseErrorHandler);
var openAiAudioApi = new OpenAiAudioApi(resolved.baseUrl(), resolved.apiKey(), resolved.headers(),
restClientBuilder, webClientBuilder, responseErrorHandler);
return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions());
}
@@ -239,4 +206,37 @@ public class OpenAiAutoConfiguration {
return manager;
}
private static @NotNull ResolvedConnectionProperties resolveConnectionProperties(
OpenAiParentProperties commonProperties, OpenAiParentProperties modelProperties, String modelType) {
String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl()
: commonProperties.getBaseUrl();
String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey()
: commonProperties.getApiKey();
String projectId = StringUtils.hasText(modelProperties.getProjectId()) ? modelProperties.getProjectId()
: commonProperties.getProjectId();
String organizationId = StringUtils.hasText(modelProperties.getOrganizationId())
? modelProperties.getOrganizationId() : commonProperties.getOrganizationId();
Map<String, List<String>> connectionHeaders = new HashMap<>();
if (StringUtils.hasText(projectId)) {
connectionHeaders.put("OpenAI-Project", List.of(projectId));
}
if (StringUtils.hasText(organizationId)) {
connectionHeaders.put("OpenAI-Organization", List.of(organizationId));
}
Assert.hasText(baseUrl,
"OpenAI base URL must be set. Use the connection property: spring.ai.openai.base-url or spring.ai.openai."
+ modelType + ".base-url property.");
Assert.hasText(apiKey,
"OpenAI API key must be set. Use the connection property: spring.ai.openai.api-key or spring.ai.openai."
+ modelType + ".api-key property.");
return new ResolvedConnectionProperties(baseUrl, apiKey, CollectionUtils.toMultiValueMap(connectionHeaders));
}
private record ResolvedConnectionProperties(String baseUrl, String apiKey, MultiValueMap<String, String> headers) {
}
}

View File

@@ -27,6 +27,10 @@ class OpenAiParentProperties {
private String baseUrl;
private String projectId;
private String organizationId;
public String getApiKey() {
return apiKey;
}
@@ -43,4 +47,20 @@ class OpenAiParentProperties {
this.baseUrl = baseUrl;
}
public String getProjectId() {
return this.projectId;
}
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public String getOrganizationId() {
return this.organizationId;
}
public void setOrganizationId(String organizationId) {
this.organizationId = organizationId;
}
}