Migrate Nomic, Jina, OVH AI, and Judge0 from Retrofit/OkHttp to the HttpClient abstraction (#5773)
## Change Migrates four 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). This removes the retrofit, okhttp, and converter-jackson dependencies from each module and routes HTTP through the pluggable, backend-agnostic client (defaulting to the JDK client at runtime). Each module's duplicated Request/Response logging interceptors are deleted in favor of the abstraction's built-in LoggingHttpClient. langchain4j-nomic - NomicClient rewritten on HttpClient; deleted NomicApi + 2 interceptors. - Added NomicJsonUtils carrying the exact ObjectMapper config the Retrofit converter used (SNAKE_CASE + NON_NULL + lenient) — the DTOs have no @JsonProperty, so this preserves the wire format byte-for-byte. - Exposed httpClientBuilder(...) on NomicEmbeddingModel, and wired the logger option through to the client (see note below). langchain4j-jina - JinaClient rewritten (endpoints v1/embeddings, multimodal, rerank); deleted JinaApi + 2 interceptors. - Added JinaJsonUtils (plain mapper — the DTOs self-annotate with @JsonNaming(SnakeCase)). - Exposed httpClientBuilder(...) on both JinaEmbeddingModel and JinaScoringModel (logger was already wired). langchain4j-ovh-ai (module is @Deprecated(forRemoval)) - Minimal transport swap only: DefaultOvhAiClient rewritten on HttpClient, OvhAiJsonUtils added, OvhAiApi + 2 interceptors deleted. No new public API — the deprecated surface is left frozen. Preserved the @JsonValue request (raw array body) and float[][] response. langchain4j-code-execution-engine-judge0 - Judge0JavaScriptEngine (raw OkHttp) rewritten on HttpClient, keeping its retry loop and per-status friendly messages. Non-2xx now surfaces as HttpException (caught → mapped to the same messages, no retry); network/timeout errors are retried; response parsing is kept outside the retry catch so parse errors propagate exactly as before. Class is package-private, so no public API change. Each affected module got a revapi.json entry for the intentional additions/removals (HttpClientBuilder exposure; removal of the public JinaApi/OvhAiApi interfaces), 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. Judge0 still returns its friendly strings. ## 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 and main modules, and they are all green - [ ] I have added/updated the documentation - [ ] I have added an example in the examples repo (only for "big" features) - [ ] I have added/updated Spring Boot starter(s) (if applicable)
This commit is contained in:
parent
e15a3f89b0
commit
b3da4a12c4
|
|
@ -22,8 +22,16 @@
|
|||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-http-client-jdk</artifactId>
|
||||
<version>1.18.0-SNAPSHOT</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package dev.langchain4j.code.judge0;
|
||||
|
||||
import static dev.langchain4j.http.client.HttpMethod.POST;
|
||||
import static dev.langchain4j.internal.Utils.isNullOrBlank;
|
||||
|
||||
import dev.langchain4j.code.CodeExecutionEngine;
|
||||
import java.io.IOException;
|
||||
import dev.langchain4j.exception.HttpException;
|
||||
import dev.langchain4j.http.client.HttpClient;
|
||||
import dev.langchain4j.http.client.HttpRequest;
|
||||
import dev.langchain4j.http.client.HttpClientBuilderLoader;
|
||||
import dev.langchain4j.http.client.SuccessfulHttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import okhttp3.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -23,7 +27,6 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
private static final Logger log = LoggerFactory.getLogger(Judge0JavaScriptEngine.class);
|
||||
|
||||
// HTTP Constants
|
||||
private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");
|
||||
private static final String RAPID_API_HOST = "judge0-ce.p.rapidapi.com";
|
||||
private static final String API_URL =
|
||||
"https://judge0-ce.p.rapidapi.com/submissions?base64_encoded=true&wait=true&fields=*";
|
||||
|
|
@ -46,7 +49,7 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
|
||||
private final String apiKey;
|
||||
private final int languageId;
|
||||
private final OkHttpClient client;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Creates a new Judge0JavaScriptEngine with the specified API key, language ID, and timeout.
|
||||
|
|
@ -65,11 +68,9 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
|
||||
this.apiKey = apiKey;
|
||||
this.languageId = languageId;
|
||||
this.client = new OkHttpClient.Builder()
|
||||
this.httpClient = HttpClientBuilderLoader.loadHttpClientBuilder()
|
||||
.connectTimeout(timeout)
|
||||
.readTimeout(timeout)
|
||||
.writeTimeout(timeout)
|
||||
.callTimeout(timeout)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
@ -87,8 +88,7 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
|
||||
String base64EncodedCode = Base64.getEncoder().encodeToString(code.getBytes(StandardCharsets.UTF_8));
|
||||
Submission submission = new Submission(languageId, base64EncodedCode);
|
||||
RequestBody requestBody = RequestBody.create(Json.toJson(submission), MEDIA_TYPE_JSON);
|
||||
Request request = buildRequest(requestBody);
|
||||
HttpRequest request = buildRequest(Json.toJson(submission));
|
||||
|
||||
return executeWithRetry(request);
|
||||
}
|
||||
|
|
@ -96,15 +96,17 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
/**
|
||||
* Builds a request to the Judge0 API.
|
||||
*
|
||||
* @param requestBody The request body
|
||||
* @param body The request body
|
||||
* @return The built request
|
||||
*/
|
||||
private Request buildRequest(RequestBody requestBody) {
|
||||
return new Request.Builder()
|
||||
private HttpRequest buildRequest(String body) {
|
||||
return HttpRequest.builder()
|
||||
.method(POST)
|
||||
.url(API_URL)
|
||||
.addHeader("x-rapidapi-host", RAPID_API_HOST)
|
||||
.addHeader("x-rapidapi-key", apiKey)
|
||||
.post(requestBody)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
@ -114,8 +116,8 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
* @param request The request to execute
|
||||
* @return The result of the request or an error message
|
||||
*/
|
||||
private String executeWithRetry(Request request) {
|
||||
Exception lastException = null;
|
||||
private String executeWithRetry(HttpRequest request) {
|
||||
RuntimeException lastException = null;
|
||||
|
||||
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
if (attempt > 0) {
|
||||
|
|
@ -128,56 +130,44 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine {
|
|||
}
|
||||
}
|
||||
|
||||
SuccessfulHttpResponse response;
|
||||
try {
|
||||
return processRequest(request);
|
||||
} catch (IOException e) {
|
||||
response = httpClient.execute(request);
|
||||
} catch (HttpException e) {
|
||||
return handleErrorResponse(e);
|
||||
} catch (RuntimeException e) {
|
||||
lastException = e;
|
||||
log.warn("Request failed (attempt {}/{}): {}", attempt + 1, MAX_RETRIES, e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
String responseBody = response.body();
|
||||
if (isNullOrBlank(responseBody)) {
|
||||
log.warn(ERROR_NULL_RESPONSE);
|
||||
return ERROR_NULL_RESPONSE;
|
||||
}
|
||||
return processResponseBody(responseBody);
|
||||
}
|
||||
|
||||
log.error("All retry attempts failed", lastException);
|
||||
return "Failed after " + MAX_RETRIES + " attempts: " + lastException.getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single request to the Judge0 API.
|
||||
*
|
||||
* @param request The request to process
|
||||
* @return The result of the request or an error message
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
private String processRequest(Request request) throws IOException {
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
return handleErrorResponse(response);
|
||||
}
|
||||
|
||||
if (response.body() == null) {
|
||||
log.warn(ERROR_NULL_RESPONSE);
|
||||
return ERROR_NULL_RESPONSE;
|
||||
}
|
||||
|
||||
String responseBody = response.body().string();
|
||||
return processResponseBody(responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles error responses from the Judge0 API.
|
||||
*
|
||||
* @param response The error response
|
||||
* @param exception The HTTP error carrying the status code and response body
|
||||
* @return An appropriate error message
|
||||
*/
|
||||
private String handleErrorResponse(Response response) {
|
||||
private String handleErrorResponse(HttpException exception) {
|
||||
String errorMessage =
|
||||
switch (response.code()) {
|
||||
switch (exception.statusCode()) {
|
||||
case 429 -> ERROR_RATE_LIMIT;
|
||||
case 403 -> ERROR_FORBIDDEN;
|
||||
case 404 -> ERROR_NOT_FOUND;
|
||||
case 500 -> ERROR_SERVER;
|
||||
case 503 -> ERROR_UNAVAILABLE;
|
||||
default -> "Unexpected error code " + response.code() + ": " + response.message();
|
||||
default -> "Unexpected error code " + exception.statusCode() + ": " + exception.getMessage();
|
||||
};
|
||||
|
||||
log.warn(errorMessage);
|
||||
|
|
|
|||
|
|
@ -20,13 +20,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.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>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,16 @@
|
|||
"new": "method retrofit2.Call<dev.langchain4j.model.jina.internal.api.JinaEmbeddingResponse> dev.langchain4j.model.jina.internal.api.JinaApi::embedMultimodal(dev.langchain4j.model.jina.internal.api.JinaMultimodalEmbeddingRequest, java.lang.String)",
|
||||
"justification": "JinaApi is an internal Retrofit API interface (internal.api package), not part of the public API"
|
||||
},
|
||||
{
|
||||
"code": "java.class.removed",
|
||||
"old": "interface dev.langchain4j.model.jina.internal.api.JinaApi",
|
||||
"justification": "Internal Retrofit API interface (internal.api package) removed as part of migrating the HTTP transport to the langchain4j HttpClient abstraction; not part of the public API"
|
||||
},
|
||||
{
|
||||
"code": "java.class.externalClassExposedInAPI",
|
||||
"new": "missing-class dev.langchain4j.http.client.HttpClientBuilder",
|
||||
"justification": "HttpClientBuilder is intentionally exposed by the Jina integration for custom HTTP client configuration"
|
||||
},
|
||||
{
|
||||
"code": "java.method.removed",
|
||||
"old": "method dev.langchain4j.model.output.Response<java.util.List<dev.langchain4j.data.embedding.Embedding>> dev.langchain4j.model.jina.JinaEmbeddingModel::embedAll(java.util.List<dev.langchain4j.data.segment.TextSegment>)",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import dev.langchain4j.data.message.ContentType;
|
|||
import dev.langchain4j.data.message.ImageContent;
|
||||
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;
|
||||
|
|
@ -73,6 +74,7 @@ public class JinaEmbeddingModel extends DimensionAwareEmbeddingModel {
|
|||
|
||||
public JinaEmbeddingModel(JinaEmbeddingModelBuilder builder) {
|
||||
this.client = JinaClient.builder()
|
||||
.httpClientBuilder(builder.httpClientBuilder)
|
||||
.baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL))
|
||||
.apiKey(builder.apiKey)
|
||||
.timeout(getOrDefault(builder.timeout, ofSeconds(60)))
|
||||
|
|
@ -199,6 +201,7 @@ public class JinaEmbeddingModel extends DimensionAwareEmbeddingModel {
|
|||
private Boolean logRequests;
|
||||
private Boolean logResponses;
|
||||
private Logger logger;
|
||||
private HttpClientBuilder httpClientBuilder;
|
||||
private List<EmbeddingModelListener> listeners;
|
||||
|
||||
JinaEmbeddingModelBuilder() {
|
||||
|
|
@ -258,6 +261,18 @@ public class JinaEmbeddingModel extends DimensionAwareEmbeddingModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 JinaEmbeddingModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) {
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JinaEmbeddingModel build() {
|
||||
return new JinaEmbeddingModel(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.jina.internal.api.JinaRerankingRequest;
|
||||
import dev.langchain4j.model.jina.internal.api.JinaRerankingResponse;
|
||||
import dev.langchain4j.model.jina.internal.client.JinaClient;
|
||||
|
|
@ -52,6 +53,7 @@ public class JinaScoringModel implements ScoringModel {
|
|||
|
||||
public JinaScoringModel(JinaScoringModelBuilder builder) {
|
||||
this.client = JinaClient.builder()
|
||||
.httpClientBuilder(builder.httpClientBuilder)
|
||||
.baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL))
|
||||
.apiKey(ensureNotBlank(builder.apiKey, "apiKey"))
|
||||
.timeout(getOrDefault(builder.timeout, ofSeconds(60)))
|
||||
|
|
@ -97,6 +99,7 @@ public class JinaScoringModel implements ScoringModel {
|
|||
private Boolean logRequests;
|
||||
private Boolean logResponses;
|
||||
private Logger logger;
|
||||
private HttpClientBuilder httpClientBuilder;
|
||||
|
||||
JinaScoringModelBuilder() {}
|
||||
|
||||
|
|
@ -144,6 +147,18 @@ public class JinaScoringModel implements ScoringModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 JinaScoringModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) {
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JinaScoringModel build() {
|
||||
return new JinaScoringModel(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
package dev.langchain4j.model.jina.internal.api;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
public interface JinaApi {
|
||||
|
||||
@POST("v1/embeddings")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<JinaEmbeddingResponse> embed(@Body JinaEmbeddingRequest request,
|
||||
@Header("Authorization") String authorizationHeader);
|
||||
|
||||
@POST("v1/embeddings")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<JinaEmbeddingResponse> embedMultimodal(@Body JinaMultimodalEmbeddingRequest request,
|
||||
@Header("Authorization") String authorizationHeader);
|
||||
|
||||
@POST("rerank")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<JinaRerankingResponse> rerank(@Body JinaRerankingRequest request,
|
||||
@Header("Authorization") String authorizationHeader);
|
||||
}
|
||||
|
|
@ -1,53 +1,50 @@
|
|||
package dev.langchain4j.model.jina.internal.client;
|
||||
|
||||
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
|
||||
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.jina.internal.client.JinaJsonUtils.fromJson;
|
||||
import static dev.langchain4j.model.jina.internal.client.JinaJsonUtils.toJson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.langchain4j.internal.Utils;
|
||||
import dev.langchain4j.model.jina.internal.api.JinaApi;
|
||||
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 dev.langchain4j.model.jina.internal.api.JinaEmbeddingRequest;
|
||||
import dev.langchain4j.model.jina.internal.api.JinaEmbeddingResponse;
|
||||
import dev.langchain4j.model.jina.internal.api.JinaMultimodalEmbeddingRequest;
|
||||
import dev.langchain4j.model.jina.internal.api.JinaRerankingRequest;
|
||||
import dev.langchain4j.model.jina.internal.api.JinaRerankingResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.jackson.JacksonConverterFactory;
|
||||
|
||||
public class JinaClient {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT);
|
||||
|
||||
private final JinaApi jinaApi;
|
||||
private final HttpClient httpClient;
|
||||
private final String baseUrl;
|
||||
private final String authorizationHeader;
|
||||
|
||||
JinaClient(
|
||||
String baseUrl, String apiKey, Duration timeout, boolean logRequests, boolean logResponses, Logger logger) {
|
||||
JinaClient(JinaClientBuilder builder) {
|
||||
HttpClientBuilder httpClientBuilder =
|
||||
getOrDefault(builder.httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder);
|
||||
|
||||
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
|
||||
.callTimeout(timeout)
|
||||
.connectTimeout(timeout)
|
||||
.readTimeout(timeout)
|
||||
.writeTimeout(timeout);
|
||||
|
||||
if (logRequests) {
|
||||
okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(logger));
|
||||
}
|
||||
if (logResponses) {
|
||||
okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(logger));
|
||||
}
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(Utils.ensureTrailingForwardSlash(baseUrl))
|
||||
.client(okHttpClientBuilder.build())
|
||||
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER))
|
||||
HttpClient httpClient = httpClientBuilder
|
||||
.connectTimeout(builder.timeout)
|
||||
.readTimeout(builder.timeout)
|
||||
.build();
|
||||
|
||||
this.jinaApi = retrofit.create(JinaApi.class);
|
||||
this.authorizationHeader = "Bearer " + ensureNotBlank(apiKey, "apiKey");
|
||||
if (builder.logRequests || 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 JinaClientBuilder builder() {
|
||||
|
|
@ -55,54 +52,29 @@ public class JinaClient {
|
|||
}
|
||||
|
||||
public JinaEmbeddingResponse embed(JinaEmbeddingRequest request) {
|
||||
try {
|
||||
retrofit2.Response<JinaEmbeddingResponse> retrofitResponse =
|
||||
jinaApi.embed(request, authorizationHeader).execute();
|
||||
if (retrofitResponse.isSuccessful()) {
|
||||
return retrofitResponse.body();
|
||||
} else {
|
||||
throw toException(retrofitResponse);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return post("v1/embeddings", request, JinaEmbeddingResponse.class);
|
||||
}
|
||||
|
||||
public JinaEmbeddingResponse embedMultimodal(
|
||||
dev.langchain4j.model.jina.internal.api.JinaMultimodalEmbeddingRequest request) {
|
||||
try {
|
||||
retrofit2.Response<JinaEmbeddingResponse> retrofitResponse =
|
||||
jinaApi.embedMultimodal(request, authorizationHeader).execute();
|
||||
if (retrofitResponse.isSuccessful()) {
|
||||
return retrofitResponse.body();
|
||||
} else {
|
||||
throw toException(retrofitResponse);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
public JinaEmbeddingResponse embedMultimodal(JinaMultimodalEmbeddingRequest request) {
|
||||
return post("v1/embeddings", request, JinaEmbeddingResponse.class);
|
||||
}
|
||||
|
||||
public JinaRerankingResponse rerank(JinaRerankingRequest request) {
|
||||
try {
|
||||
retrofit2.Response<JinaRerankingResponse> retrofitResponse =
|
||||
jinaApi.rerank(request, authorizationHeader).execute();
|
||||
|
||||
if (retrofitResponse.isSuccessful()) {
|
||||
return retrofitResponse.body();
|
||||
} else {
|
||||
throw toException(retrofitResponse);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return post("rerank", request, JinaRerankingResponse.class);
|
||||
}
|
||||
|
||||
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);
|
||||
private <T> T post(String path, Object request, Class<T> responseType) {
|
||||
HttpRequest httpRequest = HttpRequest.builder()
|
||||
.method(POST)
|
||||
.url(baseUrl + path)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("Authorization", authorizationHeader)
|
||||
.body(toJson(request))
|
||||
.build();
|
||||
|
||||
SuccessfulHttpResponse response = httpClient.execute(httpRequest);
|
||||
|
||||
return fromJson(response.body(), responseType);
|
||||
}
|
||||
|
||||
public static class JinaClientBuilder {
|
||||
|
|
@ -112,6 +84,7 @@ public class JinaClient {
|
|||
private boolean logRequests;
|
||||
private boolean logResponses;
|
||||
private Logger logger;
|
||||
private HttpClientBuilder httpClientBuilder;
|
||||
|
||||
JinaClientBuilder() {}
|
||||
|
||||
|
|
@ -145,9 +118,13 @@ public class JinaClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
public JinaClientBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) {
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JinaClient build() {
|
||||
return new JinaClient(
|
||||
this.baseUrl, this.apiKey, this.timeout, this.logRequests, this.logResponses, this.logger);
|
||||
return new JinaClient(this);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.langchain4j.model.jina.internal.client;
|
||||
|
||||
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
class JinaJsonUtils {
|
||||
|
||||
private JinaJsonUtils() throws InstantiationException {
|
||||
throw new InstantiationException("Can't instantiate this utility class.");
|
||||
}
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
package dev.langchain4j.model.jina.internal.client;
|
||||
|
||||
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,50 +0,0 @@
|
|||
package dev.langchain4j.model.jina.internal.client;
|
||||
|
||||
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;
|
||||
|
||||
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(),
|
||||
RequestLoggingInterceptor.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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,18 +20,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 Nomic integration for custom HTTP client configuration"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package dev.langchain4j.model.nomic;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
interface NomicApi {
|
||||
|
||||
@POST("embedding/text")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<EmbeddingResponse> embed(@Body EmbeddingRequest request, @Header("Authorization") String authorizationHeader);
|
||||
}
|
||||
|
|
@ -1,55 +1,46 @@
|
|||
package dev.langchain4j.model.nomic;
|
||||
|
||||
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 java.io.IOException;
|
||||
import java.time.Duration;
|
||||
|
||||
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.nomic.NomicJsonUtils.fromJson;
|
||||
import static dev.langchain4j.model.nomic.NomicJsonUtils.toJson;
|
||||
|
||||
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.time.Duration;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
class NomicClient {
|
||||
|
||||
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 NomicApi nomicApi;
|
||||
private final HttpClient httpClient;
|
||||
private final String baseUrl;
|
||||
private final String authorizationHeader;
|
||||
|
||||
NomicClient(String baseUrl, String apiKey, Duration timeout, Boolean logRequests, Boolean logResponses, Logger logger) {
|
||||
NomicClient(NomicClientBuilder builder) {
|
||||
HttpClientBuilder httpClientBuilder =
|
||||
getOrDefault(builder.httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder);
|
||||
|
||||
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
|
||||
.callTimeout(timeout)
|
||||
.connectTimeout(timeout)
|
||||
.readTimeout(timeout)
|
||||
.writeTimeout(timeout);
|
||||
|
||||
if (logRequests) {
|
||||
okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(logger));
|
||||
}
|
||||
if (logResponses) {
|
||||
okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(logger));
|
||||
}
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(Utils.ensureTrailingForwardSlash(baseUrl))
|
||||
.client(okHttpClientBuilder.build())
|
||||
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER))
|
||||
HttpClient httpClient = httpClientBuilder
|
||||
.connectTimeout(builder.timeout)
|
||||
.readTimeout(builder.timeout)
|
||||
.build();
|
||||
|
||||
this.nomicApi = retrofit.create(NomicApi.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 NomicClientBuilder builder() {
|
||||
|
|
@ -57,25 +48,17 @@ class NomicClient {
|
|||
}
|
||||
|
||||
public EmbeddingResponse embed(EmbeddingRequest request) {
|
||||
try {
|
||||
retrofit2.Response<EmbeddingResponse> retrofitResponse
|
||||
= nomicApi.embed(request, authorizationHeader).execute();
|
||||
HttpRequest httpRequest = HttpRequest.builder()
|
||||
.method(POST)
|
||||
.url(baseUrl + "embedding/text")
|
||||
.addHeader("Content-Type", "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(), EmbeddingResponse.class);
|
||||
}
|
||||
|
||||
public static class NomicClientBuilder {
|
||||
|
|
@ -85,6 +68,7 @@ class NomicClient {
|
|||
private Boolean logRequests;
|
||||
private Boolean logResponses;
|
||||
private Logger logger;
|
||||
private HttpClientBuilder httpClientBuilder;
|
||||
|
||||
NomicClientBuilder() {
|
||||
}
|
||||
|
|
@ -119,8 +103,13 @@ class NomicClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
public NomicClientBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) {
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NomicClient build() {
|
||||
return new NomicClient(this.baseUrl, this.apiKey, this.timeout, this.logRequests, this.logResponses, this.logger);
|
||||
return new NomicClient(this);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import static java.util.stream.Collectors.toList;
|
|||
|
||||
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.output.Response;
|
||||
import dev.langchain4j.model.output.TokenUsage;
|
||||
|
|
@ -56,11 +57,13 @@ public class NomicEmbeddingModel extends DimensionAwareEmbeddingModel {
|
|||
|
||||
public NomicEmbeddingModel(NomicEmbeddingModelBuilder builder) {
|
||||
this.client = NomicClient.builder()
|
||||
.httpClientBuilder(builder.httpClientBuilder)
|
||||
.baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL))
|
||||
.apiKey(ensureNotBlank(builder.apiKey, "apiKey"))
|
||||
.timeout(getOrDefault(builder.timeout, ofSeconds(60)))
|
||||
.logRequests(getOrDefault(builder.logRequests, false))
|
||||
.logResponses(getOrDefault(builder.logResponses, false))
|
||||
.logger(builder.logger)
|
||||
.build();
|
||||
this.modelName = ensureNotBlank(builder.modelName, "modelName");
|
||||
this.taskType = builder.taskType;
|
||||
|
|
@ -130,6 +133,7 @@ public class NomicEmbeddingModel extends DimensionAwareEmbeddingModel {
|
|||
private Boolean logRequests;
|
||||
private Boolean logResponses;
|
||||
private Logger logger;
|
||||
private HttpClientBuilder httpClientBuilder;
|
||||
|
||||
NomicEmbeddingModelBuilder() {}
|
||||
|
||||
|
|
@ -187,6 +191,18 @@ public class NomicEmbeddingModel extends DimensionAwareEmbeddingModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 NomicEmbeddingModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) {
|
||||
this.httpClientBuilder = httpClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NomicEmbeddingModel build() {
|
||||
return new NomicEmbeddingModel(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package dev.langchain4j.model.nomic;
|
||||
|
||||
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 NomicJsonUtils {
|
||||
|
||||
private NomicJsonUtils() 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
package dev.langchain4j.model.nomic;
|
||||
|
||||
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-]{5})([\\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.nomic;
|
||||
|
||||
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.nomic.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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,13 +25,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.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.removed",
|
||||
"old": "interface dev.langchain4j.model.ovhai.internal.api.OvhAiApi",
|
||||
"justification": "Internal Retrofit API interface (internal.api package) removed as part of migrating the HTTP transport to the langchain4j HttpClient abstraction; not part of the public API"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package dev.langchain4j.model.ovhai.internal.api;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.14.0")
|
||||
public interface OvhAiApi {
|
||||
|
||||
@POST("api/batch_text2vec")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<List<float[]>> embed(@Body EmbeddingRequest request, @Header("Authorization") String authorizationHeader);
|
||||
}
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
package dev.langchain4j.model.ovhai.internal.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.langchain4j.internal.Utils;
|
||||
import static dev.langchain4j.http.client.HttpMethod.POST;
|
||||
import static dev.langchain4j.internal.Utils.ensureTrailingForwardSlash;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.model.ovhai.internal.client.OvhAiJsonUtils.fromJson;
|
||||
import static dev.langchain4j.model.ovhai.internal.client.OvhAiJsonUtils.toJson;
|
||||
|
||||
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 dev.langchain4j.model.ovhai.internal.api.EmbeddingRequest;
|
||||
import dev.langchain4j.model.ovhai.internal.api.EmbeddingResponse;
|
||||
import dev.langchain4j.model.ovhai.internal.api.OvhAiApi;
|
||||
import okhttp3.OkHttpClient;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.jackson.JacksonConverterFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead
|
||||
|
|
@ -21,13 +22,8 @@ import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
|||
@Deprecated(forRemoval = true, since = "1.14.0")
|
||||
public class DefaultOvhAiClient extends OvhAiClient {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT);
|
||||
|
||||
private final OkHttpClient okHttpClient;
|
||||
|
||||
private final String apiKey;
|
||||
private final boolean logResponses;
|
||||
private final OvhAiApi ovhAiApi;
|
||||
private final HttpClient httpClient;
|
||||
private final String baseUrl;
|
||||
private final String authorizationHeader;
|
||||
|
||||
public static Builder builder() {
|
||||
|
|
@ -44,52 +40,36 @@ public class DefaultOvhAiClient extends OvhAiClient {
|
|||
DefaultOvhAiClient(Builder builder) {
|
||||
ensureNotBlank(builder.apiKey, "%s", "OVHcloud API key must be defined. It can be generated here: https://endpoints.ai.cloud.ovh.net/");
|
||||
|
||||
this.apiKey = builder.apiKey;
|
||||
this.logResponses = builder.logResponses;
|
||||
HttpClientBuilder httpClientBuilder = HttpClientBuilderLoader.loadHttpClientBuilder();
|
||||
HttpClient httpClient = httpClientBuilder
|
||||
.connectTimeout(builder.timeout)
|
||||
.readTimeout(builder.timeout)
|
||||
.build();
|
||||
|
||||
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
|
||||
.callTimeout(builder.timeout)
|
||||
.connectTimeout(builder.timeout)
|
||||
.readTimeout(builder.timeout)
|
||||
.writeTimeout(builder.timeout);
|
||||
|
||||
if (builder.logRequests) {
|
||||
okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(builder.logger));
|
||||
}
|
||||
if (logResponses) {
|
||||
okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(builder.logger));
|
||||
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.okHttpClient = okHttpClientBuilder.build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(Utils.ensureTrailingForwardSlash(ensureNotBlank(builder.baseUrl, "baseUrl")))
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER))
|
||||
.build();
|
||||
|
||||
this.ovhAiApi = retrofit.create(OvhAiApi.class);
|
||||
this.authorizationHeader = "Bearer " + ensureNotBlank(apiKey, "apiKey");
|
||||
this.baseUrl = ensureTrailingForwardSlash(ensureNotBlank(builder.baseUrl, "baseUrl"));
|
||||
this.authorizationHeader = "Bearer " + ensureNotBlank(builder.apiKey, "apiKey");
|
||||
}
|
||||
|
||||
public EmbeddingResponse embed(EmbeddingRequest request) {
|
||||
try {
|
||||
retrofit2.Response<List<float[]>> retrofitResponse = ovhAiApi.embed(request, authorizationHeader).execute();
|
||||
HttpRequest httpRequest = HttpRequest.builder()
|
||||
.method(POST)
|
||||
.url(baseUrl + "api/batch_text2vec")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("Authorization", authorizationHeader)
|
||||
.body(toJson(request))
|
||||
.build();
|
||||
|
||||
if (retrofitResponse.isSuccessful()) {
|
||||
return new EmbeddingResponse(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);
|
||||
float[][] embeddings = fromJson(response.body(), float[][].class);
|
||||
return new EmbeddingResponse(Arrays.asList(embeddings));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.langchain4j.model.ovhai.internal.client;
|
||||
|
||||
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
class OvhAiJsonUtils {
|
||||
|
||||
private OvhAiJsonUtils() throws InstantiationException {
|
||||
throw new InstantiationException("Can't instantiate this utility class.");
|
||||
}
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package dev.langchain4j.model.ovhai.internal.client;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.14.0")
|
||||
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-]{5})([\\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,54 +0,0 @@
|
|||
package dev.langchain4j.model.ovhai.internal.client;
|
||||
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static dev.langchain4j.internal.Utils.getOrDefault;
|
||||
import static dev.langchain4j.model.ovhai.internal.client.RequestLoggingInterceptor.inOneLine;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.14.0")
|
||||
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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue