
io.quarkiverse.langchain4j.anthropic.AnthropicRestApi Maven / Gradle / Ivy
package io.quarkiverse.langchain4j.anthropic;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import jakarta.annotation.Priority;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.ext.WriterInterceptorContext;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.jboss.resteasy.reactive.RestStreamElementType;
import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import dev.langchain4j.model.anthropic.internal.api.AnthropicCreateMessageRequest;
import dev.langchain4j.model.anthropic.internal.api.AnthropicCreateMessageResponse;
import dev.langchain4j.model.anthropic.internal.api.AnthropicStreamingData;
import io.quarkiverse.langchain4j.QuarkusJsonCodecFactory;
import io.smallrye.mutiny.Multi;
@Path("")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RegisterProvider(AnthropicRestApi.AnthropicRestApiJacksonReader.class)
@RegisterProvider(AnthropicRestApi.AnthropicRestApiJacksonWriter.class)
@RegisterProvider(AnthropicRestApi.AnthropicRestApiWriterInterceptor.class)
public interface AnthropicRestApi {
String API_KEY_HEADER = "x-api-key";
@Path("/messages")
@POST
AnthropicCreateMessageResponse createMessage(AnthropicCreateMessageRequest request, @BeanParam ApiMetadata apiMetadata);
@Path("/messages")
@POST
@RestStreamElementType(MediaType.APPLICATION_JSON)
Multi streamMessage(AnthropicCreateMessageRequest request, @BeanParam ApiMetadata apiMetadata);
class ApiMetadata {
@HeaderParam(API_KEY_HEADER)
public final String apiKey;
@HeaderParam("anthropic-version")
public final String anthropicVersion;
@HeaderParam("anthropic-beta")
public final String beta;
private ApiMetadata(String apiKey, String anthropicVersion, String beta) {
if ((apiKey == null) || apiKey.isBlank()) {
throw new IllegalArgumentException("apiKey cannot be null or blank");
}
if ((anthropicVersion == null) || anthropicVersion.isBlank()) {
throw new IllegalArgumentException("anthropicVersion cannot be null or blank");
}
this.apiKey = apiKey;
this.anthropicVersion = anthropicVersion;
this.beta = beta;
}
public static ApiMetadata.Builder builder() {
return new Builder();
}
public static class Builder {
private String apiKey;
private String anthropicVersion;
private String beta;
public ApiMetadata build() {
return new ApiMetadata(this.apiKey, this.anthropicVersion, this.beta);
}
public ApiMetadata.Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
public ApiMetadata.Builder anthropicVersion(String anthropicVersion) {
this.anthropicVersion = anthropicVersion;
return this;
}
public ApiMetadata.Builder beta(String beta) {
this.beta = beta;
return this;
}
}
}
/**
* The point of this is to properly set the {@code stream} value of the request
* so users don't have to remember to set it manually
*/
class AnthropicRestApiWriterInterceptor implements WriterInterceptor {
@Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
var entity = context.getEntity();
if (entity instanceof AnthropicCreateMessageRequest request) {
var headers = context.getHeaders();
var acceptList = headers.get(HttpHeaders.ACCEPT);
if ((acceptList != null) && (acceptList.size() == 1)) {
var accept = acceptList.get(0);
if (MediaType.APPLICATION_JSON.equals(accept)) {
if (Boolean.TRUE.equals(request.isStream())) {
context.setEntity(request.toBuilder().stream(false).build());
}
} else if (MediaType.SERVER_SENT_EVENTS.equals(accept)) {
if (!Boolean.TRUE.equals(request.isStream())) {
context.setEntity(request.toBuilder().stream(true).build());
}
}
}
}
context.proceed();
}
}
@Priority(Priorities.USER - 100) // this priority ensures that our Reader has priority over the standard Jackson one
class AnthropicRestApiJacksonReader extends AbstractJsonMessageBodyReader {
/**
* We need a custom version of the Jackson provider because reading SSE values does not work properly with
* {@code @ClientObjectMapper} due to the lack of a complete context in those requests
*/
@Override
public Object readFrom(Class
© 2015 - 2025 Weber Informatics LLC | Privacy Policy