Migrate Cohere, HuggingFace, and Workers AI from Retrofit/OkHttp to the HttpClient abstraction (#5780)
## 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)
This commit is contained in:
parent
707f83ed9e
commit
29878c30e3
|
|
@ -29,18 +29,21 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.retrofit2</groupId>
|
||||
<artifactId>retrofit</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client-jdk</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.retrofit2</groupId>
|
||||
<artifactId>converter-jackson</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EmbedResponse> embed(@Body EmbedRequest request, @Header("Authorization") String authorizationHeader);
|
||||
|
||||
@POST("embed")
|
||||
@Headers({"accept: application/json", "content-type: application/json"})
|
||||
Call<EmbedV2Response> embedV2(@Body EmbedV2Request request, @Header("Authorization") String authorizationHeader);
|
||||
|
||||
@POST("rerank")
|
||||
@Headers({"accept: application/json", "content-type: application/json"})
|
||||
Call<RerankResponse> rerank(@Body RerankRequest request, @Header("Authorization") String authorizationHeader);
|
||||
}
|
||||
|
|
@ -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<EmbedResponse> 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<EmbedV2Response> 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<RerankResponse> 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() {
|
||||
|
|
|
|||
|
|
@ -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<EmbeddingModelListener> listeners) {
|
||||
this.listeners = listeners;
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -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> T fromJson(String jsonStr, Class<T> clazz) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(jsonStr, clazz);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,18 +21,16 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.retrofit2</groupId>
|
||||
<artifactId>retrofit</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.retrofit2</groupId>
|
||||
<artifactId>converter-jackson</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client-jdk</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<TextGenerationResponse>> 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<TextGenerationResponse> responses =
|
||||
fromJson(httpResponse.body(), new TypeReference<List<TextGenerationResponse>>() {});
|
||||
|
||||
return toOneResponse(responses);
|
||||
}
|
||||
|
||||
private static TextGenerationResponse toOneResponse(Response<List<TextGenerationResponse>> retrofitResponse) {
|
||||
List<TextGenerationResponse> responses = retrofitResponse.body();
|
||||
private static TextGenerationResponse toOneResponse(List<TextGenerationResponse> responses) {
|
||||
if (responses != null && responses.size() == 1) {
|
||||
return responses.get(0);
|
||||
} else {
|
||||
|
|
@ -76,25 +76,16 @@ class DefaultHuggingFaceClient implements HuggingFaceClient {
|
|||
|
||||
@Override
|
||||
public List<float[]> embed(EmbeddingRequest request) {
|
||||
try {
|
||||
Response<List<float[]>> 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<List<float[]>>() {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<TextGenerationResponse>> generate(@Body TextGenerationRequest request, @Path("modelId") String modelId);
|
||||
|
||||
@POST("models/{modelId}/pipeline/feature-extraction")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<List<float[]>> embed(@Body EmbeddingRequest request, @Path("modelId") String modelId);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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> T fromJson(String jsonStr, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(jsonStr, typeReference);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,18 +23,31 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.retrofit2</groupId>
|
||||
<artifactId>retrofit</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client-jdk</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.retrofit2</groupId>
|
||||
<artifactId>converter-jackson</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
|
|
|||
|
|
@ -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::<init>()",
|
||||
"new": "method void dev.langchain4j.model.workersai.client.WorkersAiClient::<init>(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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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<dev.langchain4j.model.workersai.client.WorkersAiChatCompletionResponse> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Embedding> 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<dev.langchain4j.model.workersai.client.WorkersAiEmbeddingResponse> 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<Float> 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<Float> 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<Embedding> processChunk(List<TextSegment> chunk, String accountIdentifier, String modelName)
|
||||
throws IOException {
|
||||
private List<Embedding> processChunk(List<TextSegment> 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<dev.langchain4j.model.workersai.client.WorkersAiEmbeddingResponse> 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<List<Float>> embeddings = res.getData();
|
||||
List<Embedding> embeddingsList = new ArrayList<>();
|
||||
|
|
|
|||
|
|
@ -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<ResponseBody> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> generate(String prompt) {
|
||||
try {
|
||||
retrofit2.Response<WorkersAiTextCompletionResponse> 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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WorkersAiChatCompletionResponse> 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<WorkersAiTextCompletionResponse> 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<ResponseBody> 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<WorkersAiEmbeddingResponse> embed(@Body WorkersAiEmbeddingRequest apiRequest,
|
||||
@Path("accountIdentifier") String accountIdentifier,
|
||||
@Path(value = "modelName", encoded = true) String modelName);
|
||||
|
||||
}
|
||||
|
|
@ -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 extends ApiResponse<?>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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> T fromJson(String jsonStr, Class<T> clazz) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(jsonStr, clazz);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue