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:
Dmytro Liubarskyi 2026-07-15 22:25:12 +02:00 committed by GitHub
parent e15a3f89b0
commit b3da4a12c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 393 additions and 740 deletions

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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>)",

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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() {

View File

@ -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);
}
}
}

View File

@ -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.]";
}
}
}

View File

@ -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]";
}
}
}

View File

@ -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>

View File

@ -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"
}
]
}
}
]

View File

@ -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);
}

View File

@ -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() {

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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.]";
}
}
}

View File

@ -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]";
}
}
}

View File

@ -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>

View File

@ -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"
}
]
}
}
]

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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.]";
}
}
}

View File

@ -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]";
}
}
}