From ee696f33ddc1ef0dd1f4b0953a2178403f589333 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Fri, 19 Jul 2024 15:11:04 -0400 Subject: [PATCH] Make chat and embedding paths configurable * Updated docs * Minor code cleanup --- .../ai/openai/api/OpenAiApi.java | 42 ++++++++++++----- .../ROOT/pages/api/chat/openai-chat.adoc | 1 + .../api/embeddings/openai-embeddings.adoc | 1 + .../openai/OpenAiAutoConfiguration.java | 46 +++++++++++++------ .../openai/OpenAiChatProperties.java | 12 +++++ .../openai/OpenAiEmbeddingProperties.java | 12 +++++ 6 files changed, 89 insertions(+), 25 deletions(-) diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java index 283ae5b6e..8fb4794f8 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java @@ -15,17 +15,9 @@ */ package org.springframework.ai.openai.api; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.ai.model.ChatModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.openai.api.common.OpenAiApiConstants; @@ -39,6 +31,13 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; // @formatter:off /** @@ -56,6 +55,10 @@ public class OpenAiApi { public static final String DEFAULT_EMBEDDING_MODEL = EmbeddingModel.TEXT_EMBEDDING_ADA_002.getValue(); private static final Predicate SSE_DONE_PREDICATE = "[DONE]"::equals; + private final String completionsPath; + + private final String embeddingsPath; + private final RestClient restClient; private final WebClient webClient; @@ -99,7 +102,24 @@ public class OpenAiApi { * @param responseErrorHandler Response error handler. */ public OpenAiApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + this(baseUrl, openAiToken, "/v1/chat/completions", "/v1/embeddings", + restClientBuilder, webClientBuilder, responseErrorHandler); + } + /** + * Create a new chat completion api. + * + * @param baseUrl api base URL. + * @param openAiToken OpenAI apiKey. + * @param restClientBuilder RestClient builder. + * @param responseErrorHandler Response error handler. + */ + public OpenAiApi(String baseUrl, String openAiToken, String completionsPath, String embeddingsPath, + RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + Assert.hasText(completionsPath, "Completions Path must not be null"); + Assert.hasText(embeddingsPath, "Embeddings Path must not be null"); + this.completionsPath = completionsPath; + this.embeddingsPath = embeddingsPath; this.restClient = restClientBuilder .baseUrl(baseUrl) .defaultHeaders(ApiUtils.getJsonContentHeaders(openAiToken)) @@ -812,7 +832,7 @@ public class OpenAiApi { Assert.isTrue(!chatRequest.stream(), "Request must set the steam property to false."); return this.restClient.post() - .uri("/v1/chat/completions") + .uri(this.completionsPath) .body(chatRequest) .retrieve() .toEntity(ChatCompletion.class); @@ -834,7 +854,7 @@ public class OpenAiApi { AtomicBoolean isInsideTool = new AtomicBoolean(false); return this.webClient.post() - .uri("/v1/chat/completions") + .uri(this.completionsPath) .body(Mono.just(chatRequest), ChatCompletionRequest.class) .retrieve() .bodyToFlux(String.class) @@ -1022,7 +1042,7 @@ public class OpenAiApi { } return this.restClient.post() - .uri("/v1/embeddings") + .uri(this.embeddingsPath) .body(embeddingRequest) .retrieve() .toEntity(new ParameterizedTypeReference<>() { diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc index cf4c0009a..351f79abb 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc @@ -89,6 +89,7 @@ The prefix `spring.ai.openai.chat` is the property prefix that lets you configur | spring.ai.openai.chat.enabled | Enable OpenAI chat model. | true | 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.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 diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc index 17095eee6..1e2db6c71 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc @@ -89,6 +89,7 @@ The prefix `spring.ai.openai.embedding` is property prefix that configures the ` | spring.ai.openai.embedding.enabled | Enable OpenAI embedding model. | true | 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.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) diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java index 51bb1505c..baee94b0f 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.ai.autoconfigure.openai; import java.util.List; +import org.jetbrains.annotations.NotNull; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; @@ -71,8 +72,7 @@ public class OpenAiAutoConfiguration { FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { - var openAiApi = openAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, webClientBuilder, + var openAiApi = openAiApi(chatProperties, commonProperties, restClientBuilder, webClientBuilder, responseErrorHandler, "chat"); if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { @@ -91,17 +91,39 @@ public class OpenAiAutoConfiguration { WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { - var openAiApi = openAiApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - embeddingProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, webClientBuilder, + var openAiApi = openAiApi(embeddingProperties, commonProperties, restClientBuilder, webClientBuilder, responseErrorHandler, "embedding"); return new OpenAiEmbeddingModel(openAiApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate); } - private OpenAiApi openAiApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, + 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); + } + + 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(), + OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH, embeddingProperties.getEmbeddingsPath(), + restClientBuilder, webClientBuilder, responseErrorHandler); + } + + private static @NotNull 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, @@ -112,9 +134,10 @@ public class OpenAiAutoConfiguration { 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); + } - return new OpenAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, webClientBuilder, - responseErrorHandler); + private record ResolvedBaseUrlAndApiKey(String resolvedBaseUrl, String resolvedApiKey) { } @Bean @@ -164,10 +187,8 @@ public class OpenAiAutoConfiguration { var openAiAudioApi = new OpenAiAudioApi(baseUrl, apiKey, restClientBuilder, webClientBuilder, responseErrorHandler); - OpenAiAudioTranscriptionModel openAiChatModel = new OpenAiAudioTranscriptionModel(openAiAudioApi, - transcriptionProperties.getOptions(), retryTemplate); + return new OpenAiAudioTranscriptionModel(openAiAudioApi, transcriptionProperties.getOptions(), retryTemplate); - return openAiChatModel; } @Bean @@ -193,10 +214,7 @@ public class OpenAiAutoConfiguration { var openAiAudioApi = new OpenAiAudioApi(baseUrl, apiKey, restClientBuilder, webClientBuilder, responseErrorHandler); - OpenAiAudioSpeechModel openAiSpeechModel = new OpenAiAudioSpeechModel(openAiAudioApi, - speechProperties.getOptions(), retryTemplate); - - return openAiSpeechModel; + return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions()); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiChatProperties.java index f602f23b1..f1a301cd9 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiChatProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiChatProperties.java @@ -28,11 +28,15 @@ public class OpenAiChatProperties extends OpenAiParentProperties { private static final Double DEFAULT_TEMPERATURE = 0.7; + public static final String DEFAULT_COMPLETIONS_PATH = "/v1/chat/completions"; + /** * Enable OpenAI chat model. */ private boolean enabled = true; + private String completionsPath = DEFAULT_COMPLETIONS_PATH; + @NestedConfigurationProperty private OpenAiChatOptions options = OpenAiChatOptions.builder() .withModel(DEFAULT_CHAT_MODEL) @@ -55,4 +59,12 @@ public class OpenAiChatProperties extends OpenAiParentProperties { this.enabled = enabled; } + public String getCompletionsPath() { + return completionsPath; + } + + public void setCompletionsPath(String completionsPath) { + this.completionsPath = completionsPath; + } + } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiEmbeddingProperties.java index 5901d4013..008a3c18d 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiEmbeddingProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiEmbeddingProperties.java @@ -27,6 +27,8 @@ public class OpenAiEmbeddingProperties extends OpenAiParentProperties { public static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002"; + public static final String DEFAULT_EMBEDDINGS_PATH = "/v1/embeddings"; + /** * Enable OpenAI embedding model. */ @@ -34,6 +36,8 @@ public class OpenAiEmbeddingProperties extends OpenAiParentProperties { private MetadataMode metadataMode = MetadataMode.EMBED; + private String embeddingsPath = DEFAULT_EMBEDDINGS_PATH; + @NestedConfigurationProperty private OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder() .withModel(DEFAULT_EMBEDDING_MODEL) @@ -63,4 +67,12 @@ public class OpenAiEmbeddingProperties extends OpenAiParentProperties { this.enabled = enabled; } + public String getEmbeddingsPath() { + return embeddingsPath; + } + + public void setEmbeddingsPath(String embeddingsPath) { + this.embeddingsPath = embeddingsPath; + } + }