Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dotCMS/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,11 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-official</artifactId>
</dependency>
<dependency>
<!-- LangChain4J Anthropic provider: Chat model via the Anthropic Messages API -->
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<!-- LangChain4J Google Vertex AI provider: Chat model via Vertex AI Gemini (ADC auth) -->
<groupId>dev.langchain4j</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.dotcms.ai.client.langchain4j;

import com.dotmarketing.util.Logger;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import dev.langchain4j.model.anthropic.AnthropicStreamingChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.image.ImageModel;

import java.time.Duration;

/**
* {@link ModelProviderStrategy} implementation for Anthropic (Claude).
*
* <p>Talks directly to the Anthropic Messages API with an API key — distinct from
* accessing Claude models through AWS Bedrock. The {@code endpoint} config field
* overrides the default base URL if set (useful for proxies/gateways).
*
* <p>Model IDs use the Anthropic form, e.g. {@code claude-sonnet-4-6},
* {@code claude-opus-4-8}, {@code claude-haiku-4-5}.
*
* <p>Supports chat (streaming and non-streaming) only. Anthropic does not offer
* embeddings or image-generation APIs.
*/
class AnthropicModelProviderStrategy implements ModelProviderStrategy {

@Override
public String providerName() {
return "anthropic";
}

@Override
public ChatModel buildChatModel(final ProviderConfig config, final String modelType) {
validate(config, modelType);
final AnthropicChatModel.AnthropicChatModelBuilder builder = AnthropicChatModel.builder()
.apiKey(config.apiKey())
.modelName(config.model());
if (config.endpoint() != null) builder.baseUrl(config.endpoint());
if (config.temperature() != null) builder.temperature(config.temperature());
if (config.maxTokens() != null) builder.maxTokens(config.maxTokens());
if (config.maxRetries() != null) builder.maxRetries(config.maxRetries());
if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout()));
return builder.build();
}

@Override
public StreamingChatModel buildStreamingChatModel(final ProviderConfig config, final String modelType) {
validate(config, modelType);
final AnthropicStreamingChatModel.AnthropicStreamingChatModelBuilder builder =
AnthropicStreamingChatModel.builder()
.apiKey(config.apiKey())
.modelName(config.model());
if (config.maxRetries() != null) {
Logger.warn(AnthropicModelProviderStrategy.class,
"maxRetries is not supported by the Anthropic streaming chat model and will be ignored");
}
if (config.endpoint() != null) builder.baseUrl(config.endpoint());
if (config.temperature() != null) builder.temperature(config.temperature());
if (config.maxTokens() != null) builder.maxTokens(config.maxTokens());
if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout()));
return builder.build();
}

@Override
public EmbeddingModel buildEmbeddingModel(final ProviderConfig config, final String modelType) {
throw new UnsupportedOperationException(
"Embeddings are not supported by Anthropic (no embeddings API)");
}

@Override
public ImageModel buildImageModel(final ProviderConfig config, final String modelType) {
throw new UnsupportedOperationException(
"Image generation is not supported by Anthropic (no image API)");
}

private void validate(final ProviderConfig config, final String modelType) {
ModelProviderStrategy.requireNonBlank(config.model(), "model", modelType);
ModelProviderStrategy.requireNonBlank(config.apiKey(), "apiKey", modelType);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
* To add a new provider, create a class that implements {@link ModelProviderStrategy}
* and add an instance to {@link #STRATEGIES}. No other class needs to change.
*
* <p>Supported providers: {@code openai}, {@code azure_openai}, {@code vertex_ai}
* <p>Note: {@code vertex_ai} supports chat only; embeddings and image are not available via LangChain4J.
* <p>Supported providers: {@code openai}, {@code azure_openai}, {@code vertex_ai}, {@code anthropic}
* <p>Note: {@code vertex_ai} and {@code anthropic} support chat only; embeddings and image are not available via LangChain4J.
*/
public class LangChain4jModelFactory {

static final List<ModelProviderStrategy> STRATEGIES = List.of(
new OpenAiModelProviderStrategy(),
new AzureOpenAiModelProviderStrategy(),
new VertexAiModelProviderStrategy()
new VertexAiModelProviderStrategy(),
new AnthropicModelProviderStrategy()
);

private LangChain4jModelFactory() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*
* <p>Common fields (all providers):
* <ul>
* <li>{@code provider} – identifier: {@code openai}, {@code azure_openai}, {@code bedrock}, {@code vertex_ai}</li>
* <li>{@code provider} – identifier: {@code openai}, {@code azure_openai}, {@code bedrock}, {@code vertex_ai}, {@code anthropic}</li>
* <li>{@code model} – model name or ID</li>
* <li>{@code maxTokens} – max output tokens</li>
* <li>{@code temperature} – sampling temperature (0.0–2.0)</li>
Expand All @@ -43,6 +43,13 @@
* <li>{@code secretAccessKey}</li>
* </ul>
*
* <p>Anthropic (chat only — Anthropic has no embeddings or image APIs):
* <ul>
* <li>{@code apiKey} – Anthropic API key</li>
* <li>{@code model} – e.g. {@code claude-sonnet-4-6}, {@code claude-haiku-4-5}</li>
* <li>{@code endpoint} – optional base URL override (proxies/gateways)</li>
* </ul>
*
* <p>Google Vertex AI (chat only — embeddings and image not supported by this integration):
* <ul>
* <li>{@code projectId} – GCP project ID</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,125 @@ public void test_buildImageModel_azureOpenai_foundryEndpoint_missingModel_throws
assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildImageModel(config));
}

// ── Anthropic ─────────────────────────────────────────────────────────────

/**
* Given a valid Anthropic config,
* When buildChatModel is called,
* Then a ChatModel is returned successfully.
*/
@Test
public void test_buildChatModel_anthropic_returnsModel() {
final ChatModel model = LangChain4jModelFactory.buildChatModel(anthropicConfig("claude-sonnet-4-6"));
assertNotNull(model);
}

/**
* Given a valid Anthropic config with optional parameters,
* When buildChatModel is called,
* Then a ChatModel is returned successfully.
*/
@Test
public void test_buildChatModel_anthropic_withOptionalParams_returnsModel() {
final ProviderConfig config = ImmutableProviderConfig.builder()
.provider("anthropic")
.model("claude-sonnet-4-6")
.apiKey("test-key")
.temperature(0.7)
.maxTokens(4096)
.maxRetries(2)
.timeout(60)
.build();
assertNotNull(LangChain4jModelFactory.buildChatModel(config));
}

/**
* Given a valid Anthropic config,
* When buildStreamingChatModel is called,
* Then a StreamingChatModel is returned successfully.
*/
@Test
public void test_buildStreamingChatModel_anthropic_returnsModel() {
assertNotNull(LangChain4jModelFactory.buildStreamingChatModel(anthropicConfig("claude-sonnet-4-6")));
}

/**
* Given an Anthropic config with a custom endpoint override,
* When buildChatModel is called,
* Then a ChatModel is returned successfully.
*/
@Test
public void test_buildChatModel_anthropic_customEndpoint_returnsModel() {
final ProviderConfig config = ImmutableProviderConfig.builder()
.provider("anthropic")
.model("claude-sonnet-4-6")
.apiKey("test-key")
.endpoint("https://my-gateway.example.com/anthropic/v1")
.build();
assertNotNull(LangChain4jModelFactory.buildChatModel(config));
}

/**
* Given an Anthropic config without an apiKey,
* When buildChatModel is called,
* Then an IllegalArgumentException is thrown.
*/
@Test
public void test_buildChatModel_anthropic_missingApiKey_throws() {
final ProviderConfig config = ImmutableProviderConfig.builder()
.provider("anthropic")
.model("claude-sonnet-4-6")
.build();
assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config));
}

/**
* Given an Anthropic config without a model,
* When buildChatModel is called,
* Then an IllegalArgumentException is thrown.
*/
@Test
public void test_buildChatModel_anthropic_missingModel_throws() {
final ProviderConfig config = ImmutableProviderConfig.builder()
.provider("anthropic")
.apiKey("test-key")
.build();
assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config));
}

/**
* Given an Anthropic config,
* When buildEmbeddingModel is called,
* Then an UnsupportedOperationException is thrown since Anthropic has no embeddings API.
*/
@Test
public void test_buildEmbeddingModel_anthropic_throws() {
assertThrows(UnsupportedOperationException.class,
() -> LangChain4jModelFactory.buildEmbeddingModel(anthropicConfig("claude-sonnet-4-6")));
}

/**
* Given an Anthropic config,
* When buildImageModel is called,
* Then an UnsupportedOperationException is thrown since Anthropic has no image API.
*/
@Test
public void test_buildImageModel_anthropic_throws() {
assertThrows(UnsupportedOperationException.class,
() -> LangChain4jModelFactory.buildImageModel(anthropicConfig("claude-sonnet-4-6")));
}

// ── Helpers ───────────────────────────────────────────────────────────────

private static ProviderConfig anthropicConfig(final String model) {
return ImmutableProviderConfig.builder()
.provider("anthropic")
.model(model)
.apiKey("test-key")
.build();
}


private static ProviderConfig openAiConfig(final String model) {
return ImmutableProviderConfig.builder()
.provider("openai")
Expand Down
Loading