From 29878c30e3aca5c5c7d19eefb40f2d7dfd4bc667 Mon Sep 17 00:00:00 2001 From: Dmytro Liubarskyi Date: Thu, 16 Jul 2026 10:19:19 +0200 Subject: [PATCH] Migrate Cohere, HuggingFace, and Workers AI from Retrofit/OkHttp to the HttpClient abstraction (#5780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Change Migrates three more integrations off Retrofit/OkHttp and onto LangChain4j's own `HttpClient` abstraction (`dev.langchain4j.http.client`), consistent with the already-migrated modules (OpenAI, Anthropic, Mistral, Gemini, Voyage). Each module drops `retrofit`, `okhttp`, and `converter-jackson`, routes HTTP through the pluggable backend (default JDK client at runtime), and replaces its hand-rolled logging interceptors with the abstraction's `LoggingHttpClient`. **`langchain4j-cohere`** - `CohereClient` rewritten on `HttpClient`; added `CohereJsonUtils` mirroring the exact ObjectMapper config (`SNAKE_CASE` + `NON_NULL` + lenient — DTOs have no `@JsonProperty`, so the wire format is preserved). Endpoints unchanged: `POST embed` (v1 + v2), `POST rerank`; `Authorization: Bearer` preserved. - Deleted `CohereApi` + both interceptors. Added `httpClientBuilder(...)` to `CohereClient`, `CohereEmbeddingModel`, and `CohereScoringModel`. - `proxy(...)` (public on `CohereScoringModel` + a deprecated constructor) can't be expressed through the abstraction, so it now **throws `UnsupportedOperationException`** pointing users to `httpClientBuilder(...)` rather than silently ignoring the proxy. The builder method is `@Deprecated`. **`langchain4j-hugging-face`** - `DefaultHuggingFaceClient` rewritten on `HttpClient`; added `HuggingFaceJsonUtils` (plain mapper — DTOs self-annotate with `@JsonNaming`). The `Authorization: Bearer` header from `ApiKeyInsertingInterceptor` is replicated on each request. Endpoints unchanged. - Deleted `HuggingFaceApi` + `ApiKeyInsertingInterceptor`. The `HuggingFaceClient` interface and the `HuggingFaceClientFactory` SPI method are unchanged; `HuggingFaceClientFactory.Input` gains a **`default HttpClientBuilder httpClientBuilder()`** (backward-compatible — existing SPI implementors, e.g. quarkus-langchain4j, inherit the `null` default and are unaffected). Added `httpClientBuilder(...)` to the chat/language/embedding model builders. **`langchain4j-workers-ai`** - `WorkersAiClient` rewritten on `HttpClient`; added `WorkersAiJsonUtils`. Endpoints/paths/`Authorization` preserved. Image generation returns raw bytes via `SuccessfulHttpResponse.bodyBytes()` (no String round-trip). Cloudflare's `HTTP 200` + `{"success":false}` error envelope is still surfaced (via a restored `checkSuccess()`), alongside the automatic `HttpException` for non-2xx. - Deleted `WorkersAiApi`; added `httpClientBuilder(...)` to all four model builders. Some internal `.client` plumbing was removed (`WorkersAiClient` no-arg constructor / `createService` / `AuthInterceptor`, and `AbstractWorkersAIModel.processErrors`/`workerAiClient`) — documented in `revapi.json`. Each module got a `revapi.json` entry for the intentional `HttpClientBuilder` exposure (and removals), matching the existing convention in `langchain4j-chroma`, `langchain4j-open-ai`, `langchain4j-voyage-ai`, etc. **Behavioral note:** on non-2xx responses the clients now throw `dev.langchain4j.exception.HttpException` (a `RuntimeException` subclass) instead of a plain `RuntimeException` with a `"status code: …"` message — the same behavior all other migrated modules already exhibit. ## General checklist - [X] There are no breaking changes (API, behaviour) - [ ] I have added unit and/or integration tests for my change - [ ] The tests cover both positive and negative cases - [X] I have manually run all the unit and integration tests in the module I have added/changed, and they are all green - [ ] I have manually run all the unit and integration tests in the [core](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-core) and [main](https://github.com/langchain4j/langchain4j/tree/main/langchain4j) modules, and they are all green - [ ] I have added/updated the [documentation](https://github.com/langchain4j/langchain4j/tree/main/docs/docs) - [ ] I have added an example in the [examples repo](https://github.com/langchain4j/langchain4j-examples) (only for "big" features) - [ ] I have added/updated [Spring Boot starter(s)](https://github.com/langchain4j/langchain4j-spring) (if applicable) --- langchain4j-cohere/pom.xml | 15 +- langchain4j-cohere/revapi.json | 5 + .../langchain4j/model/cohere/CohereApi.java | 22 -- .../model/cohere/CohereClient.java | 160 ++++++------- .../model/cohere/CohereEmbeddingModel.java | 16 ++ .../model/cohere/CohereJsonUtils.java | 37 +++ .../model/cohere/CohereScoringModel.java | 24 ++ .../cohere/RequestLoggingInterceptor.java | 89 ------- .../cohere/ResponseLoggingInterceptor.java | 51 ---- langchain4j-hugging-face/pom.xml | 16 +- langchain4j-hugging-face/revapi.json | 15 ++ .../ApiKeyInsertingInterceptor.java | 29 --- .../huggingface/DefaultHuggingFaceClient.java | 93 ++++---- .../model/huggingface/FactoryCreator.java | 7 +- .../model/huggingface/HuggingFaceApi.java | 22 -- .../huggingface/HuggingFaceChatModel.java | 18 ++ .../HuggingFaceEmbeddingModel.java | 48 +++- .../huggingface/HuggingFaceJsonUtils.java | 30 +++ .../huggingface/HuggingFaceLanguageModel.java | 18 ++ .../spi/HuggingFaceClientFactory.java | 5 + langchain4j-workers-ai/pom.xml | 25 +- langchain4j-workers-ai/revapi.json | 46 ++++ .../model/workersai/WorkersAiChatModel.java | 35 +-- .../workersai/WorkersAiEmbeddingModel.java | 74 +++--- .../model/workersai/WorkersAiImageModel.java | 38 ++- .../workersai/WorkersAiLanguageModel.java | 36 +-- .../client/AbstractWorkersAIModel.java | 54 ++--- .../model/workersai/client/WorkersAiApi.java | 82 ------- .../workersai/client/WorkersAiClient.java | 221 +++++++++++++----- .../workersai/client/WorkersAiJsonUtils.java | 35 +++ 30 files changed, 729 insertions(+), 637 deletions(-) delete mode 100644 langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereApi.java create mode 100644 langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereJsonUtils.java delete mode 100644 langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/RequestLoggingInterceptor.java delete mode 100644 langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/ResponseLoggingInterceptor.java create mode 100644 langchain4j-hugging-face/revapi.json delete mode 100644 langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/ApiKeyInsertingInterceptor.java delete mode 100644 langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceApi.java create mode 100644 langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceJsonUtils.java create mode 100644 langchain4j-workers-ai/revapi.json delete mode 100644 langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiApi.java create mode 100644 langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiJsonUtils.java diff --git a/langchain4j-cohere/pom.xml b/langchain4j-cohere/pom.xml index 14263af683..4219367558 100644 --- a/langchain4j-cohere/pom.xml +++ b/langchain4j-cohere/pom.xml @@ -29,18 +29,21 @@ - com.squareup.retrofit2 - retrofit + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT - com.squareup.okhttp3 - okhttp + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime - com.squareup.retrofit2 - converter-jackson + com.fasterxml.jackson.core + jackson-databind diff --git a/langchain4j-cohere/revapi.json b/langchain4j-cohere/revapi.json index 5eea2f272e..c0a20aa691 100644 --- a/langchain4j-cohere/revapi.json +++ b/langchain4j-cohere/revapi.json @@ -33,6 +33,11 @@ "code": "java.class.externalClassExposedInAPI", "new": "missing-class dev.langchain4j.model.ModelProvider", "justification": "Core langchain4j type intentionally exposed by the EmbeddingModel request/response API (embed(EmbeddingRequest), supportedParameters(), supportedContentTypes(), provider(), listeners())" + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "missing-class dev.langchain4j.http.client.HttpClientBuilder", + "justification": "HttpClientBuilder is intentionally exposed by the Cohere integration for custom HTTP client configuration" } ] } diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereApi.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereApi.java deleted file mode 100644 index c3272888bf..0000000000 --- a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereApi.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.langchain4j.model.cohere; - -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.POST; - -interface CohereApi { - - @POST("embed") - @Headers({"accept: application/json", "content-type: application/json"}) - Call embed(@Body EmbedRequest request, @Header("Authorization") String authorizationHeader); - - @POST("embed") - @Headers({"accept: application/json", "content-type: application/json"}) - Call embedV2(@Body EmbedV2Request request, @Header("Authorization") String authorizationHeader); - - @POST("rerank") - @Headers({"accept: application/json", "content-type: application/json"}) - Call rerank(@Body RerankRequest request, @Header("Authorization") String authorizationHeader); -} diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereClient.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereClient.java index 4be7bef685..2b53efc3e6 100644 --- a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereClient.java +++ b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereClient.java @@ -1,61 +1,51 @@ package dev.langchain4j.model.cohere; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.SerializationFeature; -import dev.langchain4j.internal.Utils; -import okhttp3.OkHttpClient; -import org.slf4j.Logger; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; +import static dev.langchain4j.http.client.HttpMethod.POST; +import static dev.langchain4j.internal.Utils.ensureTrailingForwardSlash; +import static dev.langchain4j.internal.Utils.getOrDefault; +import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.cohere.CohereJsonUtils.fromJson; +import static dev.langchain4j.model.cohere.CohereJsonUtils.toJson; +import static java.time.Duration.ofSeconds; -import java.io.IOException; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpClientBuilder; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; +import dev.langchain4j.http.client.log.LoggingHttpClient; import java.net.Proxy; import java.time.Duration; -import java.util.Objects; - -import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import org.slf4j.Logger; class CohereClient { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) - .enable(SerializationFeature.INDENT_OUTPUT) - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - private final CohereApi cohereApi; + private final HttpClient httpClient; + private final String baseUrl; private final String authorizationHeader; - CohereClient(String baseUrl, String apiKey, Duration timeout, Proxy proxy, Boolean logRequests, Boolean logResponses, Logger logger) { + CohereClient(CohereClientBuilder builder) { - OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() - .callTimeout(timeout) + HttpClientBuilder httpClientBuilder = + getOrDefault(builder.httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder); + + Duration timeout = getOrDefault(builder.timeout, ofSeconds(60)); + + HttpClient httpClient = httpClientBuilder .connectTimeout(timeout) .readTimeout(timeout) - .writeTimeout(timeout); - - if (logRequests) { - okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(logger)); - } - if (logResponses) { - okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(logger)); - } - - if (Objects.nonNull(proxy)) { - okHttpClientBuilder.proxy(proxy); - } - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(Utils.ensureTrailingForwardSlash(baseUrl)) - .client(okHttpClientBuilder.build()) - .addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER)) .build(); - this.cohereApi = retrofit.create(CohereApi.class); - this.authorizationHeader = "Bearer " + ensureNotBlank(apiKey, "apiKey"); + if (builder.logRequests != null && builder.logRequests + || builder.logResponses != null && builder.logResponses) { + this.httpClient = + new LoggingHttpClient(httpClient, builder.logRequests, builder.logResponses, builder.logger); + } else { + this.httpClient = httpClient; + } + + this.baseUrl = ensureTrailingForwardSlash(builder.baseUrl); + this.authorizationHeader = "Bearer " + ensureNotBlank(builder.apiKey, "apiKey"); } public static CohereClientBuilder builder() { @@ -63,58 +53,52 @@ class CohereClient { } EmbedResponse embed(EmbedRequest request) { - try { - retrofit2.Response retrofitResponse - = cohereApi.embed(request, authorizationHeader).execute(); + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "embed") + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(request)) + .build(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + SuccessfulHttpResponse response = httpClient.execute(httpRequest); + + return fromJson(response.body(), EmbedResponse.class); } EmbedV2Response embedV2(EmbedV2Request request) { - try { - retrofit2.Response retrofitResponse = - cohereApi.embedV2(request, authorizationHeader).execute(); + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "embed") + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(request)) + .build(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + SuccessfulHttpResponse response = httpClient.execute(httpRequest); + + return fromJson(response.body(), EmbedV2Response.class); } RerankResponse rerank(RerankRequest request) { - try { - retrofit2.Response retrofitResponse - = cohereApi.rerank(request, authorizationHeader).execute(); + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "rerank") + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(request)) + .build(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } + SuccessfulHttpResponse response = httpClient.execute(httpRequest); - private static RuntimeException toException(retrofit2.Response response) throws IOException { - int code = response.code(); - String body = response.errorBody().string(); - String errorMessage = String.format("status code: %s; body: %s", code, body); - return new RuntimeException(errorMessage); + return fromJson(response.body(), RerankResponse.class); } public static class CohereClientBuilder { + private HttpClientBuilder httpClientBuilder; private String baseUrl; private String apiKey; private Duration timeout; @@ -126,6 +110,11 @@ class CohereClient { CohereClientBuilder() { } + public CohereClientBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public CohereClientBuilder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; @@ -142,6 +131,11 @@ class CohereClient { } public CohereClientBuilder proxy(Proxy proxy) { + if (proxy != null) { + throw new UnsupportedOperationException( + "Proxy configuration via proxy(...) is no longer supported. Supply a custom " + + "HttpClientBuilder via httpClientBuilder(...) to configure a proxy."); + } this.proxy = proxy; return this; } @@ -162,7 +156,7 @@ class CohereClient { } public CohereClient build() { - return new CohereClient(this.baseUrl, this.apiKey, this.timeout, this.proxy, this.logRequests, this.logResponses, this.logger); + return new CohereClient(this); } public String toString() { diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereEmbeddingModel.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereEmbeddingModel.java index 51f9482968..d1006baa85 100644 --- a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereEmbeddingModel.java +++ b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereEmbeddingModel.java @@ -14,6 +14,7 @@ import dev.langchain4j.data.message.ImageContent; import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.exception.UnsupportedFeatureException; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.ModelProvider; import dev.langchain4j.model.embedding.DimensionAwareEmbeddingModel; import dev.langchain4j.model.embedding.EmbeddingModel; @@ -84,6 +85,7 @@ public class CohereEmbeddingModel extends DimensionAwareEmbeddingModel { public CohereEmbeddingModel(CohereEmbeddingModelBuilder builder) { String baseUrl = getOrDefault(builder.baseUrl, DEFAULT_BASE_URL); this.client = CohereClient.builder() + .httpClientBuilder(builder.httpClientBuilder) .baseUrl(baseUrl) .apiKey(ensureNotBlank(builder.apiKey, "apiKey")) .timeout(getOrDefault(builder.timeout, ofSeconds(60))) @@ -92,6 +94,7 @@ public class CohereEmbeddingModel extends DimensionAwareEmbeddingModel { .logger(builder.logger) .build(); this.v2Client = CohereClient.builder() + .httpClientBuilder(builder.httpClientBuilder) .baseUrl(getOrDefault(builder.v2BaseUrl, toV2BaseUrl(baseUrl))) .apiKey(ensureNotBlank(builder.apiKey, "apiKey")) .timeout(getOrDefault(builder.timeout, ofSeconds(60))) @@ -279,6 +282,7 @@ public class CohereEmbeddingModel extends DimensionAwareEmbeddingModel { } public static class CohereEmbeddingModelBuilder { + private HttpClientBuilder httpClientBuilder; private String baseUrl; private String v2BaseUrl; private String apiKey; @@ -293,6 +297,18 @@ public class CohereEmbeddingModel extends DimensionAwareEmbeddingModel { CohereEmbeddingModelBuilder() {} + /** + * Sets a custom HTTP client builder, allowing fine-grained control over the HTTP client + * configuration such as timeouts and proxy settings. + * + * @param httpClientBuilder the HTTP client builder + * @return {@code this} + */ + public CohereEmbeddingModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public CohereEmbeddingModelBuilder listeners(List listeners) { this.listeners = listeners; return this; diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereJsonUtils.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereJsonUtils.java new file mode 100644 index 0000000000..78a6bf36b9 --- /dev/null +++ b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereJsonUtils.java @@ -0,0 +1,37 @@ +package dev.langchain4j.model.cohere; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; + +class CohereJsonUtils { + + private CohereJsonUtils() throws InstantiationException { + throw new InstantiationException("Can't instantiate this utility class."); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .enable(SerializationFeature.INDENT_OUTPUT) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String jsonStr, Class clazz) { + try { + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereScoringModel.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereScoringModel.java index 8571a75558..33f8620b80 100644 --- a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereScoringModel.java +++ b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/CohereScoringModel.java @@ -8,6 +8,7 @@ import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.toList; import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.output.Response; import dev.langchain4j.model.output.TokenUsage; import dev.langchain4j.model.scoring.ScoringModel; @@ -52,6 +53,7 @@ public class CohereScoringModel implements ScoringModel { public CohereScoringModel(CohereScoringModelBuilder builder) { this.client = CohereClient.builder() + .httpClientBuilder(builder.httpClientBuilder) .baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL)) .apiKey(ensureNotBlank(builder.apiKey, "apiKey")) .timeout(getOrDefault(builder.timeout, ofSeconds(60))) @@ -98,6 +100,7 @@ public class CohereScoringModel implements ScoringModel { } public static class CohereScoringModelBuilder { + private HttpClientBuilder httpClientBuilder; private String baseUrl; private String apiKey; private String modelName; @@ -110,6 +113,18 @@ public class CohereScoringModel implements ScoringModel { CohereScoringModelBuilder() {} + /** + * Sets a custom HTTP client builder, allowing fine-grained control over the HTTP client + * configuration such as timeouts and proxy settings. + * + * @param httpClientBuilder the HTTP client builder + * @return {@code this} + */ + public CohereScoringModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public CohereScoringModelBuilder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; @@ -135,6 +150,15 @@ public class CohereScoringModel implements ScoringModel { return this; } + /** + * @param proxy the proxy (no longer applied) + * @return {@code this} + * @deprecated Proxy configuration via {@code proxy(...)} is no longer supported since the migration to the + * langchain4j HttpClient abstraction. Passing a non-null proxy will cause an + * {@link UnsupportedOperationException} when the model is built. To configure a proxy, supply a custom + * {@link HttpClientBuilder} via {@link #httpClientBuilder(HttpClientBuilder)} instead. + */ + @Deprecated public CohereScoringModelBuilder proxy(Proxy proxy) { this.proxy = proxy; return this; diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/RequestLoggingInterceptor.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/RequestLoggingInterceptor.java deleted file mode 100644 index 61098cda49..0000000000 --- a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/RequestLoggingInterceptor.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.langchain4j.model.cohere; - -import okhttp3.Headers; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import okio.Buffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static java.util.stream.StreamSupport.stream; - -class RequestLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(RequestLoggingInterceptor.class); - - private static final Pattern BEARER_PATTERN = Pattern.compile("(Bearer\\s)(\\w{2})(\\w+)(\\w{2})"); - - private final Logger log; - - RequestLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - log(request); - return chain.proceed(request); - } - - private void log(Request request) { - log.debug( - "Request:\n" + - "- method: {}\n" + - "- url: {}\n" + - "- headers: {}\n" + - "- body: {}", - request.method(), - request.url(), - inOneLine(request.headers()), - getBody(request) - ); - } - - static String inOneLine(Headers headers) { - return stream(headers.spliterator(), false) - .map((header) -> { - String headerKey = header.component1(); - String headerValue = header.component2(); - if (headerKey.equals("Authorization")) { - headerValue = maskAuthorizationHeaderValue(headerValue); - } - return String.format("[%s: %s]", headerKey, headerValue); - }).collect(Collectors.joining(", ")); - } - - private static String maskAuthorizationHeaderValue(String authorizationHeaderValue) { - try { - Matcher matcher = BEARER_PATTERN.matcher(authorizationHeaderValue); - StringBuffer sb = new StringBuffer(); - - while (matcher.find()) { - matcher.appendReplacement(sb, matcher.group(1) + matcher.group(2) + "..." + matcher.group(4)); - } - - matcher.appendTail(sb); - return sb.toString(); - } catch (Exception e) { - return "[failed to mask the API key]"; - } - } - - private String getBody(Request request) { - try { - Buffer buffer = new Buffer(); - request.body().writeTo(buffer); - return buffer.readUtf8(); - } catch (Exception e) { - log.warn("Exception happened while reading request body", e); - return "[Exception happened while reading request body. Check logs for more details.]"; - } - } -} diff --git a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/ResponseLoggingInterceptor.java b/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/ResponseLoggingInterceptor.java deleted file mode 100644 index 781400d02d..0000000000 --- a/langchain4j-cohere/src/main/java/dev/langchain4j/model/cohere/ResponseLoggingInterceptor.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.langchain4j.model.cohere; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static dev.langchain4j.model.cohere.RequestLoggingInterceptor.inOneLine; - -class ResponseLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(ResponseLoggingInterceptor.class); - - private final Logger log; - - ResponseLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = chain.proceed(request); - log(response); - return response; - } - - void log(Response response) { - log.debug( - "Response:\n" + - "- status code: {}\n" + - "- headers: {}\n" + - "- body: {}", - response.code(), - inOneLine(response.headers()), - getBody(response) - ); - } - - private String getBody(Response response) { - try { - return response.peekBody(Long.MAX_VALUE).string(); - } catch (IOException e) { - log.warn("Failed to log response", e); - return "[failed to log response]"; - } - } -} diff --git a/langchain4j-hugging-face/pom.xml b/langchain4j-hugging-face/pom.xml index 9434dbe166..fc979619e5 100644 --- a/langchain4j-hugging-face/pom.xml +++ b/langchain4j-hugging-face/pom.xml @@ -21,18 +21,16 @@ - com.squareup.retrofit2 - retrofit + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT - com.squareup.okhttp3 - okhttp - - - - com.squareup.retrofit2 - converter-jackson + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime diff --git a/langchain4j-hugging-face/revapi.json b/langchain4j-hugging-face/revapi.json new file mode 100644 index 0000000000..ff3cd280b4 --- /dev/null +++ b/langchain4j-hugging-face/revapi.json @@ -0,0 +1,15 @@ +[ + { + "extension": "revapi.differences", + "configuration": { + "ignore": true, + "differences": [ + { + "code": "java.class.externalClassExposedInAPI", + "new": "missing-class dev.langchain4j.http.client.HttpClientBuilder", + "justification": "HttpClientBuilder is intentionally exposed by the HuggingFace integration for custom HTTP client configuration" + } + ] + } + } +] diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/ApiKeyInsertingInterceptor.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/ApiKeyInsertingInterceptor.java deleted file mode 100644 index b159976069..0000000000 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/ApiKeyInsertingInterceptor.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.langchain4j.model.huggingface; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -import java.io.IOException; - -import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; - -class ApiKeyInsertingInterceptor implements Interceptor { - - private final String apiKey; - - ApiKeyInsertingInterceptor(String apiKey) { - this.apiKey = ensureNotBlank(apiKey, "apiKey"); - } - - @Override - public Response intercept(Chain chain) throws IOException { - - Request request = chain.request() - .newBuilder() - .addHeader("Authorization", "Bearer " + apiKey) - .build(); - - return chain.proceed(request); - } -} diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/DefaultHuggingFaceClient.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/DefaultHuggingFaceClient.java index bf8fbf129a..39069ac47f 100644 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/DefaultHuggingFaceClient.java +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/DefaultHuggingFaceClient.java @@ -1,45 +1,44 @@ package dev.langchain4j.model.huggingface; +import static dev.langchain4j.http.client.HttpMethod.POST; import static dev.langchain4j.internal.Utils.ensureTrailingForwardSlash; +import static dev.langchain4j.internal.Utils.getOrDefault; import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.huggingface.HuggingFaceJsonUtils.fromJson; +import static dev.langchain4j.model.huggingface.HuggingFaceJsonUtils.toJson; +import com.fasterxml.jackson.core.type.TypeReference; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpClientBuilder; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; import dev.langchain4j.model.huggingface.client.EmbeddingRequest; import dev.langchain4j.model.huggingface.client.HuggingFaceClient; import dev.langchain4j.model.huggingface.client.TextGenerationRequest; import dev.langchain4j.model.huggingface.client.TextGenerationResponse; -import java.io.IOException; import java.time.Duration; import java.util.List; import java.util.Objects; -import okhttp3.OkHttpClient; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; class DefaultHuggingFaceClient implements HuggingFaceClient { private static final String BASE_URL = "https://router.huggingface.co/hf-inference/"; - private final HuggingFaceApi huggingFaceApi; + private final HttpClient httpClient; + private final String baseUrl; + private final String apiKey; private final String modelId; - DefaultHuggingFaceClient(String baseUrl, String apiKey, String modelId, Duration timeout) { + DefaultHuggingFaceClient( + HttpClientBuilder httpClientBuilder, String baseUrl, String apiKey, String modelId, Duration timeout) { - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new ApiKeyInsertingInterceptor(apiKey)) - .callTimeout(timeout) - .connectTimeout(timeout) - .readTimeout(timeout) - .writeTimeout(timeout) - .build(); + HttpClientBuilder builder = getOrDefault(httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(ensureTrailingForwardSlash(Objects.isNull(baseUrl) ? BASE_URL : baseUrl)) - .client(okHttpClient) - .addConverterFactory(JacksonConverterFactory.create()) - .build(); + this.httpClient = builder.connectTimeout(timeout).readTimeout(timeout).build(); - this.huggingFaceApi = retrofit.create(HuggingFaceApi.class); + this.baseUrl = ensureTrailingForwardSlash(Objects.isNull(baseUrl) ? BASE_URL : baseUrl); + this.apiKey = ensureNotBlank(apiKey, "apiKey"); this.modelId = ensureNotBlank(modelId, "modelId"); } @@ -50,22 +49,23 @@ class DefaultHuggingFaceClient implements HuggingFaceClient { @Override public TextGenerationResponse generate(TextGenerationRequest request) { - try { - retrofit2.Response> retrofitResponse = - huggingFaceApi.generate(request, modelId).execute(); + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "models/" + modelId) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", "Bearer " + apiKey) + .body(toJson(request)) + .build(); - if (retrofitResponse.isSuccessful()) { - return toOneResponse(retrofitResponse); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + SuccessfulHttpResponse httpResponse = httpClient.execute(httpRequest); + + List responses = + fromJson(httpResponse.body(), new TypeReference>() {}); + + return toOneResponse(responses); } - private static TextGenerationResponse toOneResponse(Response> retrofitResponse) { - List responses = retrofitResponse.body(); + private static TextGenerationResponse toOneResponse(List responses) { if (responses != null && responses.size() == 1) { return responses.get(0); } else { @@ -76,25 +76,16 @@ class DefaultHuggingFaceClient implements HuggingFaceClient { @Override public List embed(EmbeddingRequest request) { - try { - Response> retrofitResponse = - huggingFaceApi.embed(request, modelId).execute(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "models/" + modelId + "/pipeline/feature-extraction") + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", "Bearer " + apiKey) + .body(toJson(request)) + .build(); - private static RuntimeException toException(retrofit2.Response response) throws IOException { + SuccessfulHttpResponse httpResponse = httpClient.execute(httpRequest); - int code = response.code(); - String body = response.errorBody().string(); - - String errorMessage = String.format("status code: %s; body: %s", code, body); - return new RuntimeException(errorMessage); + return fromJson(httpResponse.body(), new TypeReference>() {}); } } diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/FactoryCreator.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/FactoryCreator.java index 7c93c1c1f4..6894d42bab 100644 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/FactoryCreator.java +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/FactoryCreator.java @@ -20,7 +20,12 @@ class FactoryCreator { @Override public HuggingFaceClient create(Input input) { - return new DefaultHuggingFaceClient(input.baseUrl(), input.apiKey(), input.modelId(), input.timeout()); + return new DefaultHuggingFaceClient( + input.httpClientBuilder(), + input.baseUrl(), + input.apiKey(), + input.modelId(), + input.timeout()); } } } diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceApi.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceApi.java deleted file mode 100644 index b2fd7ce94d..0000000000 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceApi.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.langchain4j.model.huggingface; - -import dev.langchain4j.model.huggingface.client.EmbeddingRequest; -import dev.langchain4j.model.huggingface.client.TextGenerationRequest; -import dev.langchain4j.model.huggingface.client.TextGenerationResponse; -import java.util.List; -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.Headers; -import retrofit2.http.POST; -import retrofit2.http.Path; - -interface HuggingFaceApi { - - @POST("models/{modelId}") - @Headers({"Content-Type: application/json"}) - Call> generate(@Body TextGenerationRequest request, @Path("modelId") String modelId); - - @POST("models/{modelId}/pipeline/feature-extraction") - @Headers({"Content-Type: application/json"}) - Call> embed(@Body EmbeddingRequest request, @Path("modelId") String modelId); -} diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceChatModel.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceChatModel.java index b626780b55..a2a151fcaa 100644 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceChatModel.java +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceChatModel.java @@ -10,6 +10,7 @@ import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.internal.ChatRequestValidationUtils; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; @@ -103,6 +104,11 @@ public class HuggingFaceChatModel implements ChatModel { public Duration timeout() { return builder.timeout; } + + @Override + public HttpClientBuilder httpClientBuilder() { + return builder.httpClientBuilder; + } }); this.temperature = builder.temperature; this.maxNewTokens = builder.maxNewTokens; @@ -168,6 +174,7 @@ public class HuggingFaceChatModel implements ChatModel { public static final class Builder { + private HttpClientBuilder httpClientBuilder; private String baseUrl; private String accessToken; private String modelId; @@ -177,6 +184,17 @@ public class HuggingFaceChatModel implements ChatModel { private Boolean returnFullText = false; private Boolean waitForModel = true; + /** + * Sets the HTTP client builder that will be used to create the HTTP client to communicate with HuggingFace. + * + * @param httpClientBuilder the HTTP client builder to use + * @return {@code this} + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceEmbeddingModel.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceEmbeddingModel.java index db7cf2d550..1ccc71788e 100644 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceEmbeddingModel.java +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceEmbeddingModel.java @@ -2,6 +2,7 @@ package dev.langchain4j.model.huggingface; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.embedding.DimensionAwareEmbeddingModel; import dev.langchain4j.model.huggingface.client.EmbeddingRequest; import dev.langchain4j.model.huggingface.client.HuggingFaceClient; @@ -30,18 +31,32 @@ public class HuggingFaceEmbeddingModel extends DimensionAwareEmbeddingModel { */ public HuggingFaceEmbeddingModel( String baseUrl, String accessToken, String modelId, Boolean waitForModel, Duration timeout) { - ensureNotBlank(accessToken, "%s", "HuggingFace access token must be defined. It can be generated here: https://huggingface.co/settings/tokens"); - this.waitForModel = waitForModel == null || waitForModel; - this.baseUrl = baseUrl; - this.modelId = modelId; - this.client = createClient(accessToken, modelId, timeout); + this(new HuggingFaceEmbeddingModelBuilder() + .baseUrl(baseUrl) + .accessToken(accessToken) + .modelId(modelId) + .waitForModel(waitForModel) + .timeout(timeout)); } public HuggingFaceEmbeddingModel(String accessToken, String modelId, Boolean waitForModel, Duration timeout) { - this(null, accessToken, modelId, waitForModel, timeout); + this(new HuggingFaceEmbeddingModelBuilder() + .accessToken(accessToken) + .modelId(modelId) + .waitForModel(waitForModel) + .timeout(timeout)); } - private HuggingFaceClient createClient(String accessToken, String modelId, Duration timeout) { + public HuggingFaceEmbeddingModel(HuggingFaceEmbeddingModelBuilder builder) { + ensureNotBlank(builder.accessToken, "%s", "HuggingFace access token must be defined. It can be generated here: https://huggingface.co/settings/tokens"); + this.waitForModel = builder.waitForModel == null || builder.waitForModel; + this.baseUrl = builder.baseUrl; + this.modelId = builder.modelId; + this.client = createClient(builder.httpClientBuilder, builder.accessToken, builder.modelId, builder.timeout); + } + + private HuggingFaceClient createClient( + HttpClientBuilder httpClientBuilder, String accessToken, String modelId, Duration timeout) { return FactoryCreator.FACTORY.create(new HuggingFaceClientFactory.Input() { @Override public String baseUrl() { @@ -62,6 +77,11 @@ public class HuggingFaceEmbeddingModel extends DimensionAwareEmbeddingModel { public Duration timeout() { return timeout == null ? DEFAULT_TIMEOUT : timeout; } + + @Override + public HttpClientBuilder httpClientBuilder() { + return httpClientBuilder; + } }); } @@ -97,6 +117,7 @@ public class HuggingFaceEmbeddingModel extends DimensionAwareEmbeddingModel { } public static class HuggingFaceEmbeddingModelBuilder { + private HttpClientBuilder httpClientBuilder; private String baseUrl; private String accessToken; private String modelId; @@ -107,6 +128,17 @@ public class HuggingFaceEmbeddingModel extends DimensionAwareEmbeddingModel { // This is public so it can be extended } + /** + * Sets the HTTP client builder that will be used to create the HTTP client to communicate with HuggingFace. + * + * @param httpClientBuilder the HTTP client builder to use + * @return {@code this} + */ + public HuggingFaceEmbeddingModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public HuggingFaceEmbeddingModelBuilder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; @@ -133,7 +165,7 @@ public class HuggingFaceEmbeddingModel extends DimensionAwareEmbeddingModel { } public HuggingFaceEmbeddingModel build() { - return new HuggingFaceEmbeddingModel(this.baseUrl, this.accessToken, this.modelId, this.waitForModel, this.timeout); + return new HuggingFaceEmbeddingModel(this); } public String toString() { diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceJsonUtils.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceJsonUtils.java new file mode 100644 index 0000000000..426ea0680e --- /dev/null +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceJsonUtils.java @@ -0,0 +1,30 @@ +package dev.langchain4j.model.huggingface; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +class HuggingFaceJsonUtils { + + private HuggingFaceJsonUtils() throws InstantiationException { + throw new InstantiationException("Can't instantiate this utility class."); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String jsonStr, TypeReference typeReference) { + try { + return OBJECT_MAPPER.readValue(jsonStr, typeReference); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceLanguageModel.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceLanguageModel.java index dfb881d457..fea7097e01 100644 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceLanguageModel.java +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/HuggingFaceLanguageModel.java @@ -3,6 +3,7 @@ package dev.langchain4j.model.huggingface; import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; import static dev.langchain4j.spi.ServiceHelper.loadFactories; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.huggingface.client.HuggingFaceClient; import dev.langchain4j.model.huggingface.client.Options; import dev.langchain4j.model.huggingface.client.Parameters; @@ -92,6 +93,11 @@ public class HuggingFaceLanguageModel implements LanguageModel { public Duration timeout() { return builder.timeout; } + + @Override + public HttpClientBuilder httpClientBuilder() { + return builder.httpClientBuilder; + } }); this.temperature = builder.temperature; this.maxNewTokens = builder.maxNewTokens; @@ -127,6 +133,7 @@ public class HuggingFaceLanguageModel implements LanguageModel { public static final class Builder { + private HttpClientBuilder httpClientBuilder; private String baseUrl; private String accessToken; private String modelId; @@ -136,6 +143,17 @@ public class HuggingFaceLanguageModel implements LanguageModel { private Boolean returnFullText = false; private Boolean waitForModel = true; + /** + * Sets the HTTP client builder that will be used to create the HTTP client to communicate with HuggingFace. + * + * @param httpClientBuilder the HTTP client builder to use + * @return {@code this} + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; diff --git a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/spi/HuggingFaceClientFactory.java b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/spi/HuggingFaceClientFactory.java index e578c9b8e3..fd1f50d3c4 100644 --- a/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/spi/HuggingFaceClientFactory.java +++ b/langchain4j-hugging-face/src/main/java/dev/langchain4j/model/huggingface/spi/HuggingFaceClientFactory.java @@ -1,5 +1,6 @@ package dev.langchain4j.model.huggingface.spi; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.huggingface.client.HuggingFaceClient; import java.time.Duration; @@ -19,5 +20,9 @@ public interface HuggingFaceClientFactory { String modelId(); Duration timeout(); + + default HttpClientBuilder httpClientBuilder() { + return null; + } } } diff --git a/langchain4j-workers-ai/pom.xml b/langchain4j-workers-ai/pom.xml index cff98536e2..a8ebf10665 100644 --- a/langchain4j-workers-ai/pom.xml +++ b/langchain4j-workers-ai/pom.xml @@ -23,18 +23,31 @@ - com.squareup.retrofit2 - retrofit + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT - com.squareup.okhttp3 - okhttp + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime - com.squareup.retrofit2 - converter-jackson + com.fasterxml.jackson.core + jackson-annotations + + + + com.fasterxml.jackson.core + jackson-core + + + + com.fasterxml.jackson.core + jackson-databind diff --git a/langchain4j-workers-ai/revapi.json b/langchain4j-workers-ai/revapi.json new file mode 100644 index 0000000000..6ba4618d9c --- /dev/null +++ b/langchain4j-workers-ai/revapi.json @@ -0,0 +1,46 @@ +[ + { + "extension": "revapi.differences", + "configuration": { + "ignore": true, + "differences": [ + { + "code": "java.class.externalClassExposedInAPI", + "new": "missing-class dev.langchain4j.http.client.HttpClientBuilder", + "justification": "HttpClientBuilder is intentionally exposed by the Workers AI integration for custom HTTP client configuration" + }, + { + "code": "java.class.removed", + "old": "interface dev.langchain4j.model.workersai.client.WorkersAiApi", + "justification": "Internal Retrofit API interface removed as part of the migration from Retrofit/OkHttp to the langchain4j HttpClient abstraction" + }, + { + "code": "java.class.removed", + "old": "class dev.langchain4j.model.workersai.client.WorkersAiClient.AuthInterceptor", + "justification": "OkHttp-specific interceptor removed as part of the migration from Retrofit/OkHttp to the langchain4j HttpClient abstraction; authorization is now handled via a request header" + }, + { + "code": "java.method.removed", + "old": "method dev.langchain4j.model.workersai.client.WorkersAiApi dev.langchain4j.model.workersai.client.WorkersAiClient::createService(java.lang.String)", + "justification": "Retrofit service factory removed as part of the migration from Retrofit/OkHttp to the langchain4j HttpClient abstraction; the client is now created via WorkersAiClient.builder()" + }, + { + "code": "java.method.visibilityReduced", + "old": "method void dev.langchain4j.model.workersai.client.WorkersAiClient::()", + "new": "method void dev.langchain4j.model.workersai.client.WorkersAiClient::(dev.langchain4j.model.workersai.client.WorkersAiClient.Builder)", + "justification": "The public no-arg constructor was replaced by a package-private builder-based constructor as part of the migration from Retrofit/OkHttp; WorkersAiClient is now instantiated via WorkersAiClient.builder()" + }, + { + "code": "java.method.removed", + "old": "method void dev.langchain4j.model.workersai.client.AbstractWorkersAIModel::processErrors(dev.langchain4j.model.workersai.client.ApiResponse, okhttp3.ResponseBody) throws java.io.IOException", + "justification": "Manual OkHttp error handling removed; non-2xx responses now throw dev.langchain4j.exception.HttpException automatically via the HttpClient abstraction" + }, + { + "code": "java.field.removed", + "old": "field dev.langchain4j.model.workersai.client.AbstractWorkersAIModel.workerAiClient", + "justification": "The Retrofit-based WorkersAiApi field was replaced by the HttpClient-based WorkersAiClient field as part of the migration" + } + ] + } + } +] diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiChatModel.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiChatModel.java index 08c6266c59..bdb944084d 100644 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiChatModel.java +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiChatModel.java @@ -6,6 +6,7 @@ import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.ToolExecutionResultMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.exception.UnsupportedFeatureException; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequestParameters; @@ -19,7 +20,6 @@ import dev.langchain4j.model.workersai.client.WorkersAiChatCompletionRequest; import dev.langchain4j.model.workersai.spi.WorkersAiChatModelBuilderFactory; import org.slf4j.Logger; -import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -39,7 +39,7 @@ public class WorkersAiChatModel extends AbstractWorkersAIModel implements ChatMo * @param builder builder. */ public WorkersAiChatModel(Builder builder) { - this(builder.accountId, builder.modelName, builder.apiToken); + super(builder.accountId, builder.modelName, builder.apiToken, builder.httpClientBuilder); } /** @@ -82,6 +82,10 @@ public class WorkersAiChatModel extends AbstractWorkersAIModel implements ChatMo * ModelName, preferred as enum for extensibility. */ public String modelName; + /** + * The HTTP client builder used to create the underlying HTTP client. + */ + public HttpClientBuilder httpClientBuilder; /** * Simple constructor. @@ -122,6 +126,17 @@ public class WorkersAiChatModel extends AbstractWorkersAIModel implements ChatMo return this; } + /** + * Sets the {@link HttpClientBuilder} used to create the underlying HTTP client. + * + * @param httpClientBuilder The HTTP client builder to set. + * @return The current instance of {@link Builder}. + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + /** * Builds a new instance of Worker AI Chat Model. * @@ -199,17 +214,11 @@ public class WorkersAiChatModel extends AbstractWorkersAIModel implements ChatMo * @return text generated by the model */ private String generate(WorkersAiChatCompletionRequest req) { - try { - retrofit2.Response retrofitResponse = workerAiClient - .generateChat(req, accountId, modelName) - .execute(); - processErrors(retrofitResponse.body(), retrofitResponse.errorBody()); - if (retrofitResponse.body() == null) { - throw new IllegalStateException("Response is empty"); - } - return retrofitResponse.body().getResult().getResponse(); - } catch (IOException e) { - throw new RuntimeException(e); + dev.langchain4j.model.workersai.client.WorkersAiChatCompletionResponse response = + client.generateChat(req, accountId, modelName); + if (response == null || response.getResult() == null) { + throw new IllegalStateException("Response is empty"); } + return response.getResult().getResponse(); } } diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiEmbeddingModel.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiEmbeddingModel.java index 7117498f18..4563cd533c 100644 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiEmbeddingModel.java +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiEmbeddingModel.java @@ -2,6 +2,7 @@ package dev.langchain4j.model.workersai; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.output.FinishReason; import dev.langchain4j.model.output.Response; @@ -10,7 +11,6 @@ import dev.langchain4j.model.workersai.client.WorkersAiEmbeddingResponse; import dev.langchain4j.model.workersai.spi.WorkersAiEmbeddingModelBuilderFactory; import org.slf4j.Logger; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @@ -35,7 +35,7 @@ public class WorkersAiEmbeddingModel extends AbstractWorkersAIModel implements E * @param builder builder. */ public WorkersAiEmbeddingModel(Builder builder) { - this(builder.accountId, builder.modelName, builder.apiToken); + super(builder.accountId, builder.modelName, builder.apiToken, builder.httpClientBuilder); } /** @@ -78,6 +78,10 @@ public class WorkersAiEmbeddingModel extends AbstractWorkersAIModel implements E * ModelName, preferred as enum for extensibility. */ public String modelName; + /** + * The HTTP client builder used to create the underlying HTTP client. + */ + public HttpClientBuilder httpClientBuilder; /** * Simple constructor. @@ -118,6 +122,17 @@ public class WorkersAiEmbeddingModel extends AbstractWorkersAIModel implements E return this; } + /** + * Sets the {@link HttpClientBuilder} used to create the underlying HTTP client. + * + * @param httpClientBuilder The HTTP client builder to set. + * @return The current instance of {@link Builder}. + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + /** * Builds a new instance of Worker AI Chat Model. * @@ -133,32 +148,26 @@ public class WorkersAiEmbeddingModel extends AbstractWorkersAIModel implements E */ @Override public Response embed(String text) { - try { - dev.langchain4j.model.workersai.client.WorkersAiEmbeddingRequest req = new dev.langchain4j.model.workersai.client.WorkersAiEmbeddingRequest(); - req.getText().add(text); + dev.langchain4j.model.workersai.client.WorkersAiEmbeddingRequest req = new dev.langchain4j.model.workersai.client.WorkersAiEmbeddingRequest(); + req.getText().add(text); - retrofit2.Response retrofitResponse = workerAiClient - .embed(req, accountId, modelName) - .execute(); + dev.langchain4j.model.workersai.client.WorkersAiEmbeddingResponse response = + client.embed(req, accountId, modelName); - processErrors(retrofitResponse.body(), retrofitResponse.errorBody()); - if (retrofitResponse.body() == null) { - throw new RuntimeException("Unexpected response: " + retrofitResponse); - } - dev.langchain4j.model.workersai.client.WorkersAiEmbeddingResponse.EmbeddingResult res = retrofitResponse.body().getResult(); - // Single Vector expected - if (res.getShape().get(0) != 1) { - throw new RuntimeException("Unexpected shape: " + res.getShape()); - } - List embeddings = res.getData().get(0); - float[] floatArray = new float[embeddings.size()]; - for (int i = 0; i < embeddings.size(); i++) { - floatArray[i] = embeddings.get(i); // Unboxing Float to float - } - return new Response<>(new Embedding(floatArray), null, FinishReason.STOP); - } catch (IOException e) { - throw new RuntimeException(e); + if (response == null || response.getResult() == null) { + throw new RuntimeException("Unexpected response: " + response); } + dev.langchain4j.model.workersai.client.WorkersAiEmbeddingResponse.EmbeddingResult res = response.getResult(); + // Single Vector expected + if (res.getShape().get(0) != 1) { + throw new RuntimeException("Unexpected shape: " + res.getShape()); + } + List embeddings = res.getData().get(0); + float[] floatArray = new float[embeddings.size()]; + for (int i = 0; i < embeddings.size(); i++) { + floatArray[i] = embeddings.get(i); // Unboxing Float to float + } + return new Response<>(new Embedding(floatArray), null, FinishReason.STOP); } /** @@ -216,22 +225,17 @@ public class WorkersAiEmbeddingModel extends AbstractWorkersAIModel implements E * @param accountIdentifier account identifier. * @param modelName model name. * @return list of embeddings. - * @throws IOException error occurred during invocation. */ - private List processChunk(List chunk, String accountIdentifier, String modelName) - throws IOException { + private List processChunk(List chunk, String accountIdentifier, String modelName) { dev.langchain4j.model.workersai.client.WorkersAiEmbeddingRequest req = new dev.langchain4j.model.workersai.client.WorkersAiEmbeddingRequest(); for (TextSegment textSegment : chunk) { req.getText().add(textSegment.text()); } - retrofit2.Response retrofitResponse = workerAiClient - .embed(req, accountIdentifier, modelName) - .execute(); - processErrors(retrofitResponse.body(), retrofitResponse.errorBody()); - if (retrofitResponse.body() == null) { - throw new RuntimeException("Unexpected response: " + retrofitResponse); + WorkersAiEmbeddingResponse response = client.embed(req, accountIdentifier, modelName); + if (response == null || response.getResult() == null) { + throw new RuntimeException("Unexpected response: " + response); } - WorkersAiEmbeddingResponse.EmbeddingResult res = retrofitResponse.body().getResult(); + WorkersAiEmbeddingResponse.EmbeddingResult res = response.getResult(); List> embeddings = res.getData(); List embeddingsList = new ArrayList<>(); diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiImageModel.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiImageModel.java index 1f24c9f3f1..2a74237a26 100644 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiImageModel.java +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiImageModel.java @@ -1,21 +1,19 @@ package dev.langchain4j.model.workersai; import dev.langchain4j.data.image.Image; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.image.ImageModel; import dev.langchain4j.model.output.FinishReason; import dev.langchain4j.model.output.Response; import dev.langchain4j.model.workersai.client.AbstractWorkersAIModel; import dev.langchain4j.model.workersai.client.WorkersAiImageGenerationRequest; import dev.langchain4j.model.workersai.spi.WorkersAiImageModelBuilderFactory; -import okhttp3.ResponseBody; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.net.URL; import java.util.Base64; @@ -40,7 +38,7 @@ public class WorkersAiImageModel extends AbstractWorkersAIModel implements Image * builder. */ public WorkersAiImageModel(Builder builder) { - this(builder.accountId, builder.modelName, builder.apiToken); + super(builder.accountId, builder.modelName, builder.apiToken, builder.httpClientBuilder); } /** @@ -87,6 +85,10 @@ public class WorkersAiImageModel extends AbstractWorkersAIModel implements Image * ModelName, preferred as enum for extensibility. */ public String modelName; + /** + * The HTTP client builder used to create the underlying HTTP client. + */ + public HttpClientBuilder httpClientBuilder; /** * Simple constructor. @@ -129,6 +131,17 @@ public class WorkersAiImageModel extends AbstractWorkersAIModel implements Image return this; } + /** + * Sets the {@link HttpClientBuilder} used to create the underlying HTTP client. + * + * @param httpClientBuilder The HTTP client builder to set. + * @return The current instance of {@link Builder}. + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + /** * Builds a new instance of Worker AI Chat Model. * @@ -208,22 +221,7 @@ public class WorkersAiImageModel extends AbstractWorkersAIModel implements Image } } - retrofit2.Response response = workerAiClient - .generateImage(imgReq, accountId, modelName) - .execute(); - - if (response.isSuccessful() && response.body() != null) { - InputStream inputStream = response.body().byteStream(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - while ((nRead = inputStream.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); - } - buffer.flush(); - return buffer.toByteArray(); - } - throw new IllegalStateException("An error occurred while generating image."); + return client.generateImage(imgReq, accountId, modelName); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiLanguageModel.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiLanguageModel.java index 1038a762f9..2685cc8402 100644 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiLanguageModel.java +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/WorkersAiLanguageModel.java @@ -1,5 +1,6 @@ package dev.langchain4j.model.workersai; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.input.Prompt; import dev.langchain4j.model.language.LanguageModel; import dev.langchain4j.model.output.Response; @@ -9,8 +10,6 @@ import dev.langchain4j.model.workersai.client.WorkersAiTextCompletionResponse; import dev.langchain4j.model.workersai.spi.WorkersAiLanguageModelBuilderFactory; import org.slf4j.Logger; -import java.io.IOException; - import static dev.langchain4j.spi.ServiceHelper.loadFactories; /** @@ -27,7 +26,7 @@ public class WorkersAiLanguageModel extends AbstractWorkersAIModel implements La * @param builder builder. */ public WorkersAiLanguageModel(Builder builder) { - this(builder.accountId, builder.modelName, builder.apiToken); + super(builder.accountId, builder.modelName, builder.apiToken, builder.httpClientBuilder); } /** @@ -70,6 +69,10 @@ public class WorkersAiLanguageModel extends AbstractWorkersAIModel implements La * ModelName, preferred as enum for extensibility. */ public String modelName; + /** + * The HTTP client builder used to create the underlying HTTP client. + */ + public HttpClientBuilder httpClientBuilder; /** * Simple constructor. @@ -110,6 +113,17 @@ public class WorkersAiLanguageModel extends AbstractWorkersAIModel implements La return this; } + /** + * Sets the {@link HttpClientBuilder} used to create the underlying HTTP client. + * + * @param httpClientBuilder The HTTP client builder to set. + * @return The current instance of {@link Builder}. + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + /** * Builds a new instance of Worker AI Chat Model. * @@ -125,18 +139,12 @@ public class WorkersAiLanguageModel extends AbstractWorkersAIModel implements La */ @Override public Response generate(String prompt) { - try { - retrofit2.Response retrofitResponse = workerAiClient - .generateText(new WorkersAiTextCompletionRequest(prompt), accountId, modelName) - .execute(); - processErrors(retrofitResponse.body(), retrofitResponse.errorBody()); - if (retrofitResponse.body() == null) { - throw new RuntimeException("Empty response"); - } - return new Response<>(retrofitResponse.body().getResult().getResponse()); - } catch (IOException e) { - throw new RuntimeException(e); + WorkersAiTextCompletionResponse response = + client.generateText(new WorkersAiTextCompletionRequest(prompt), accountId, modelName); + if (response == null || response.getResult() == null) { + throw new RuntimeException("Empty response"); } + return new Response<>(response.getResult().getResponse()); } /** diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/AbstractWorkersAIModel.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/AbstractWorkersAIModel.java index 5fcb8cf0f2..f3ddb93d1d 100644 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/AbstractWorkersAIModel.java +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/AbstractWorkersAIModel.java @@ -2,10 +2,7 @@ package dev.langchain4j.model.workersai.client; import static dev.langchain4j.internal.ValidationUtils.ensureNotEmpty; -import okhttp3.ResponseBody; -import org.slf4j.Logger; - -import java.io.IOException; +import dev.langchain4j.http.client.HttpClientBuilder; /** * Abstract class for WorkerAI models as they are all initialized the same way. @@ -13,7 +10,6 @@ import java.io.IOException; */ public abstract class AbstractWorkersAIModel { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(AbstractWorkersAIModel.class); /** * Account identifier, provided by the WorkerAI platform. */ @@ -25,9 +21,9 @@ public abstract class AbstractWorkersAIModel { protected String modelName; /** - * OkHttpClient for the WorkerAI API. + * Client for the WorkerAI API. */ - protected WorkersAiApi workerAiClient; + protected WorkersAiClient client; /** * Simple constructor. @@ -37,37 +33,27 @@ public abstract class AbstractWorkersAIModel { * @param apiToken api apiToken from . */ public AbstractWorkersAIModel(String accountId, String modelName, String apiToken) { + this(accountId, modelName, apiToken, null); + } + + /** + * Constructor allowing to customize the underlying HTTP client. + * + * @param accountId account identifier. + * @param modelName model name. + * @param apiToken api token. + * @param httpClientBuilder the HTTP client builder to use, may be {@code null} to use the default one. + */ + public AbstractWorkersAIModel( + String accountId, String modelName, String apiToken, HttpClientBuilder httpClientBuilder) { ensureNotEmpty(accountId, "%s", "Account identifier should not be null or empty"); this.accountId = accountId; ensureNotEmpty(modelName, "%s", "Model name should not be null or empty"); this.modelName = modelName; ensureNotEmpty(apiToken, "%s", "Token should not be null or empty"); - this.workerAiClient = WorkersAiClient.createService(apiToken); + this.client = WorkersAiClient.builder() + .apiToken(apiToken) + .httpClientBuilder(httpClientBuilder) + .build(); } - - /** - * Process errors from the API. - * - * @param res response - * @param errors errors body from retrofit - * @throws IOException error occurred during invocation - */ - protected void processErrors(ApiResponse res, ResponseBody errors) - throws IOException { - if (res == null || !res.isSuccess()) { - StringBuilder errorMessage = new StringBuilder("Failed to generate chat message:"); - if (res == null) { - errorMessage.append(errors.string()); - } else if (res.getErrors() != null) { - errorMessage.append(res.getErrors().stream() - .map(ApiResponse.Error::getMessage) - .reduce((a, b) -> a + "\n" + b) - .orElse("")); - } - log.error(errorMessage.toString()); - throw new RuntimeException(errorMessage.toString()); - } - } - - } diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiApi.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiApi.java deleted file mode 100644 index de6dc6c06e..0000000000 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiApi.java +++ /dev/null @@ -1,82 +0,0 @@ -package dev.langchain4j.model.workersai.client; - -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.POST; -import retrofit2.http.Path; - -/** - * Public interface to interact with the WorkerAI API. - */ -public interface WorkersAiApi { - - /** - * Generate chat. - * - * @param apiRequest - * request. - * @param accountIdentifier - * account identifier. - * @param modelId - * model id. - * @return - * response. - */ - @POST("client/v4/accounts/{accountIdentifier}/ai/run/{modelName}") - Call generateChat(@Body WorkersAiChatCompletionRequest apiRequest, - @Path("accountIdentifier") String accountIdentifier, - @Path(value = "modelName", encoded = true) String modelId); - - /** - * Generate text. - * - * @param apiRequest - * request. - * @param accountIdentifier - * account identifier. - * @param modelName - * model name. - * @return - * response. - */ - @POST("client/v4/accounts/{accountIdentifier}/ai/run/{modelName}") - Call generateText(@Body WorkersAiTextCompletionRequest apiRequest, - @Path("accountIdentifier") String accountIdentifier, - @Path(value = "modelName", encoded = true) String modelName); - - /** - * Generate image. - * - * @param apiRequest - * request. - * @param accountIdentifier - * account identifier. - * @param modelName - * model name. - * @return - * response. - */ - @POST("client/v4/accounts/{accountIdentifier}/ai/run/{modelName}") - Call generateImage(@Body WorkersAiImageGenerationRequest apiRequest, - @Path("accountIdentifier") String accountIdentifier, - @Path(value = "modelName", encoded = true) String modelName); - - /** - * Generate embeddings. - * - * @param apiRequest - * request. - * @param accountIdentifier - * account identifier. - * @param modelName - * model name. - * @return - * response. - */ - @POST("client/v4/accounts/{accountIdentifier}/ai/run/{modelName}") - Call embed(@Body WorkersAiEmbeddingRequest apiRequest, - @Path("accountIdentifier") String accountIdentifier, - @Path(value = "modelName", encoded = true) String modelName); - -} diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiClient.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiClient.java index a420f5041b..7fd83e6e26 100644 --- a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiClient.java +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiClient.java @@ -1,14 +1,16 @@ package dev.langchain4j.model.workersai.client; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.jetbrains.annotations.NotNull; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; +import static dev.langchain4j.http.client.HttpMethod.POST; +import static dev.langchain4j.internal.Utils.getOrDefault; +import static dev.langchain4j.model.workersai.client.WorkersAiJsonUtils.fromJson; +import static dev.langchain4j.model.workersai.client.WorkersAiJsonUtils.toJson; +import static java.time.Duration.ofSeconds; -import java.io.IOException; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpClientBuilder; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; import java.time.Duration; /** @@ -18,69 +20,164 @@ public class WorkersAiClient { private static final String BASE_URL = "https://api.cloudflare.com/"; - /** - * Constructor. - */ - public WorkersAiClient() {} + private final HttpClient httpClient; + private final String authorizationHeader; + + WorkersAiClient(Builder builder) { + HttpClientBuilder httpClientBuilder = + getOrDefault(builder.httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder); + + // The 30s timeouts preserve the original behavior: slow, but can be needed for images. + this.httpClient = httpClientBuilder + .connectTimeout(getOrDefault(getOrDefault(builder.timeout, httpClientBuilder.connectTimeout()), ofSeconds(30))) + .readTimeout(getOrDefault(getOrDefault(builder.timeout, httpClientBuilder.readTimeout()), ofSeconds(30))) + .build(); + + this.authorizationHeader = "Bearer " + builder.apiToken; + } /** - * Initialization of okHTTP. + * Generate chat. * - * @param apiToken - * authorization token - * @return - * api + * @param apiRequest request. + * @param accountIdentifier account identifier. + * @param modelName model name. + * @return response. */ - public static WorkersAiApi createService(String apiToken) { - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new AuthInterceptor(apiToken)) - // Slow but can be needed for images - .callTimeout(Duration.ofSeconds(30)) - .readTimeout(Duration.ofSeconds(30)) - .build(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(BASE_URL) - .client(okHttpClient) - .addConverterFactory(JacksonConverterFactory.create()) - .build(); - - return retrofit.create(WorkersAiApi.class); + public WorkersAiChatCompletionResponse generateChat( + WorkersAiChatCompletionRequest apiRequest, String accountIdentifier, String modelName) { + return checkSuccess( + fromJson(execute(apiRequest, accountIdentifier, modelName).body(), WorkersAiChatCompletionResponse.class)); } /** - * An interceptor for HTTP requests to add an authorization token to the header. - * Implements the {@link Interceptor} interface. + * Generate text. + * + * @param apiRequest request. + * @param accountIdentifier account identifier. + * @param modelName model name. + * @return response. */ - public static class AuthInterceptor implements Interceptor { - private final String apiToken; - - /** - * Constructs an AuthInterceptor with a specified authorization token. - * - * @param apiToken The authorization token to be used in HTTP headers. - */ - public AuthInterceptor(String apiToken) { - this.apiToken = apiToken; - } - - /** - * Intercepts an outgoing HTTP request, adding an authorization header. - * - * @param chain The chain of request/response interceptors. - * @return The modified response after adding the authorization header. - * @throws IOException If an IO exception occurs during request processing. - */ - @NotNull - @Override - public Response intercept(Chain chain) throws IOException { - Request.Builder builder = chain - .request().newBuilder() - .header("Authorization", "Bearer " + apiToken); - Request request = builder.build(); - return chain.proceed(request); - } + public WorkersAiTextCompletionResponse generateText( + WorkersAiTextCompletionRequest apiRequest, String accountIdentifier, String modelName) { + return checkSuccess( + fromJson(execute(apiRequest, accountIdentifier, modelName).body(), WorkersAiTextCompletionResponse.class)); } + /** + * Generate image. The endpoint returns the raw binary image, so the response bytes are returned as-is. + * + * @param apiRequest request. + * @param accountIdentifier account identifier. + * @param modelName model name. + * @return the raw image bytes. + */ + public byte[] generateImage( + WorkersAiImageGenerationRequest apiRequest, String accountIdentifier, String modelName) { + return execute(apiRequest, accountIdentifier, modelName).bodyBytes(); + } + /** + * Generate embeddings. + * + * @param apiRequest request. + * @param accountIdentifier account identifier. + * @param modelName model name. + * @return response. + */ + public WorkersAiEmbeddingResponse embed( + WorkersAiEmbeddingRequest apiRequest, String accountIdentifier, String modelName) { + return checkSuccess( + fromJson(execute(apiRequest, accountIdentifier, modelName).body(), WorkersAiEmbeddingResponse.class)); + } + + /** + * Surfaces Cloudflare API errors returned in a 2xx envelope with {@code success=false}. + * Non-2xx responses are already turned into an {@link dev.langchain4j.exception.HttpException} by the HTTP client. + */ + private static > T checkSuccess(T response) { + if (response == null || !response.isSuccess()) { + StringBuilder errorMessage = new StringBuilder("Failed to generate chat message:"); + if (response != null && response.getErrors() != null) { + errorMessage.append(response.getErrors().stream() + .map(ApiResponse.Error::getMessage) + .reduce((a, b) -> a + "\n" + b) + .orElse("")); + } + throw new RuntimeException(errorMessage.toString()); + } + return response; + } + + private SuccessfulHttpResponse execute(Object apiRequest, String accountIdentifier, String modelName) { + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(BASE_URL + "client/v4/accounts/" + accountIdentifier + "/ai/run/" + modelName) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(apiRequest)) + .build(); + return httpClient.execute(httpRequest); + } + + /** + * Builder access. + * + * @return builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link WorkersAiClient}. + */ + public static class Builder { + + private HttpClientBuilder httpClientBuilder; + private Duration timeout; + private String apiToken; + + /** + * Sets the {@link HttpClientBuilder} used to create the underlying HTTP client. + * + * @param httpClientBuilder the HTTP client builder. + * @return {@code this}. + */ + public Builder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + + /** + * Sets the timeout used for both connecting and reading. + * + * @param timeout the timeout. + * @return {@code this}. + */ + public Builder timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + /** + * Sets the API token used for authorization. + * + * @param apiToken the API token. + * @return {@code this}. + */ + public Builder apiToken(String apiToken) { + this.apiToken = apiToken; + return this; + } + + /** + * Builds a new {@link WorkersAiClient}. + * + * @return a new client instance. + */ + public WorkersAiClient build() { + return new WorkersAiClient(this); + } + } } diff --git a/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiJsonUtils.java b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiJsonUtils.java new file mode 100644 index 0000000000..c2cb9b9e46 --- /dev/null +++ b/langchain4j-workers-ai/src/main/java/dev/langchain4j/model/workersai/client/WorkersAiJsonUtils.java @@ -0,0 +1,35 @@ +package dev.langchain4j.model.workersai.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Internal JSON (de)serialization helper for the Workers AI client. + * + *

The mapper is intentionally a plain {@link ObjectMapper} with default configuration, mirroring + * the behavior of the Jackson converter that was previously used by the Retrofit-based client.

+ */ +class WorkersAiJsonUtils { + + private WorkersAiJsonUtils() throws InstantiationException { + throw new InstantiationException("Can't instantiate this utility class."); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String jsonStr, Class clazz) { + try { + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +}