From ac3f97ea8a51640d3de8b6ce097f50a4a3742c1b Mon Sep 17 00:00:00 2001 From: ihoffmann-dot Date: Fri, 12 Jun 2026 00:17:23 -0300 Subject: [PATCH] feat(dotAI): add OpenRouter provider via LangChain4J OpenAI-compatible API --- .../langchain4j/LangChain4jModelFactory.java | 7 +- .../OpenRouterModelProviderStrategy.java | 90 +++++++++++++++ .../ai/client/langchain4j/ProviderConfig.java | 9 +- .../LangChain4jModelFactoryTest.java | 106 ++++++++++++++++++ 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/OpenRouterModelProviderStrategy.java diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java index 2ee704514bc..cd57067138f 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java @@ -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. * - *

Supported providers: {@code openai}, {@code azure_openai}, {@code vertex_ai} - *

Note: {@code vertex_ai} supports chat only; embeddings and image are not available via LangChain4J. + *

Supported providers: {@code openai}, {@code azure_openai}, {@code vertex_ai}, {@code openrouter} + *

Note: {@code vertex_ai} and {@code openrouter} support chat only; embeddings and image are not available via LangChain4J. */ public class LangChain4jModelFactory { static final List STRATEGIES = List.of( new OpenAiModelProviderStrategy(), new AzureOpenAiModelProviderStrategy(), - new VertexAiModelProviderStrategy() + new VertexAiModelProviderStrategy(), + new OpenRouterModelProviderStrategy() ); private LangChain4jModelFactory() {} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/OpenRouterModelProviderStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/OpenRouterModelProviderStrategy.java new file mode 100644 index 00000000000..58d17f65660 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/OpenRouterModelProviderStrategy.java @@ -0,0 +1,90 @@ +package com.dotcms.ai.client.langchain4j; + +import com.dotmarketing.util.Logger; +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 dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; + +import java.time.Duration; + +/** + * {@link ModelProviderStrategy} implementation for OpenRouter. + * + *

OpenRouter exposes an OpenAI-compatible API, so this strategy reuses the + * LangChain4J OpenAI model classes pointed at the OpenRouter base URL + * ({@value #DEFAULT_BASE_URL}). The {@code endpoint} config field overrides the + * base URL if set. + * + *

Model IDs use the OpenRouter namespaced form, e.g. {@code openai/gpt-4o}, + * {@code anthropic/claude-sonnet-4}, {@code deepseek/deepseek-r1}. + * + *

Supports chat (streaming and non-streaming) only. OpenRouter does not offer + * embeddings or image-generation endpoints. + */ +class OpenRouterModelProviderStrategy implements ModelProviderStrategy { + + static final String DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"; + + @Override + public String providerName() { + return "openrouter"; + } + + @Override + public ChatModel buildChatModel(final ProviderConfig config, final String modelType) { + validate(config, modelType); + final OpenAiChatModel.OpenAiChatModelBuilder builder = OpenAiChatModel.builder() + .apiKey(config.apiKey()) + .modelName(config.model()) + .baseUrl(baseUrl(config)); + 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 OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder = OpenAiStreamingChatModel.builder() + .apiKey(config.apiKey()) + .modelName(config.model()) + .baseUrl(baseUrl(config)); + if (config.maxRetries() != null) { + Logger.warn(OpenRouterModelProviderStrategy.class, + "maxRetries is not supported by the OpenRouter streaming chat model and will be ignored"); + } + 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 OpenRouter (no embeddings endpoint)"); + } + + @Override + public ImageModel buildImageModel(final ProviderConfig config, final String modelType) { + throw new UnsupportedOperationException( + "Image generation is not supported by OpenRouter via LangChain4J"); + } + + private void validate(final ProviderConfig config, final String modelType) { + ModelProviderStrategy.requireNonBlank(config.model(), "model", modelType); + ModelProviderStrategy.requireNonBlank(config.apiKey(), "apiKey", modelType); + } + + private static String baseUrl(final ProviderConfig config) { + return config.endpoint() != null && !config.endpoint().isBlank() + ? config.endpoint() + : DEFAULT_BASE_URL; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java index ee9b416a686..7bfe2a322c8 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java @@ -18,7 +18,7 @@ * *

Common fields (all providers): *

* + *

OpenRouter (chat only — OpenRouter has no embeddings or image endpoints): + *

+ * *

Google Vertex AI (chat only — embeddings and image not supported by this integration): *