This is an automated email from the ASF dual-hosted git repository.

ArafatKhan2198 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 7c54977e640 HDDS-15906. Add gateway LLM provider for Recon chatbot 
(OpenAI-compatible gateways). (#10805).
7c54977e640 is described below

commit 7c54977e640fd5939b417c8237a726dc71fd8145
Author: Arafat2198 <[email protected]>
AuthorDate: Sun Jul 19 14:33:40 2026 +0530

    HDDS-15906. Add gateway LLM provider for Recon chatbot (OpenAI-compatible 
gateways). (#10805).
    
    Co-authored-by: Cursor <[email protected]>
---
 .../dist/src/main/compose/ozone-ha/docker-config   | 26 ++++++++++
 .../dist/src/main/compose/ozone/docker-config      | 14 +++++-
 .../src/main/compose/ozonesecure/docker-config     | 26 ++++++++++
 .../ozone/recon/chatbot/ChatbotConfigKeys.java     | 20 ++++++++
 .../recon/chatbot/llm/LangChain4jDispatcher.java   | 27 ++++++++++
 .../chatbot/llm/TestLangChain4jDispatcher.java     | 57 ++++++++++++++++++++++
 .../ozone/recon/chatbot/llm/TestLlmRouting.java    |  8 +++
 7 files changed, 176 insertions(+), 2 deletions(-)

diff --git a/hadoop-ozone/dist/src/main/compose/ozone-ha/docker-config 
b/hadoop-ozone/dist/src/main/compose/ozone-ha/docker-config
index b2479944e9e..0ee670aabc2 100644
--- a/hadoop-ozone/dist/src/main/compose/ozone-ha/docker-config
+++ b/hadoop-ozone/dist/src/main/compose/ozone-ha/docker-config
@@ -56,3 +56,29 @@ OZONE_CONF_DIR=/etc/hadoop
 OZONE_LOG_DIR=/var/log/hadoop
 
 no_proxy=om1,om2,om3,scm,s3g,recon,kdc,localhost,127.0.0.1
+
+# Recon AI Chatbot — DISABLED by default.
+#
+# WARNING: The plaintext API key approach shown below is for LOCAL DOCKER
+# TESTING ONLY. It must NOT be used on production clusters because plaintext
+# keys are exposed via 'hadoop conf | grep api.key' and via Recon's /conf HTTP
+# endpoint. For production clusters, store the key in a Hadoop JCEKS credential
+# store instead (see ozone-site.xml.template for full setup instructions).
+#
+# To enable the chatbot locally for testing:
+#   1. Uncomment ONE block below (direct provider or gateway).
+#   2. Replace the placeholder key with a real key.
+#   3. Never commit a real key to git — rotate it immediately if you do.
+#
+# --- Direct provider (Gemini example; OpenAI and Anthropic are also 
supported) ---
+# OZONE-SITE.XML_ozone.recon.chatbot.enabled=true
+# OZONE-SITE.XML_ozone.recon.chatbot.provider=gemini
+# OZONE-SITE.XML_ozone.recon.chatbot.gemini.api.key=YOUR_GEMINI_API_KEY_HERE
+#
+# --- OpenAI-compatible gateway (LiteLLM etc.) — one door for all models ---
+# OZONE-SITE.XML_ozone.recon.chatbot.enabled=true
+# OZONE-SITE.XML_ozone.recon.chatbot.provider=gateway
+# OZONE-SITE.XML_ozone.recon.chatbot.gateway.base.url=https://your-gateway/v1
+# OZONE-SITE.XML_ozone.recon.chatbot.gateway.api.key=YOUR_GATEWAY_KEY_HERE
+# 
OZONE-SITE.XML_ozone.recon.chatbot.gateway.models=gpt-4.1,claude-sonnet,gemini-2.5-flash
+# OZONE-SITE.XML_ozone.recon.chatbot.default.model=claude-sonnet
diff --git a/hadoop-ozone/dist/src/main/compose/ozone/docker-config 
b/hadoop-ozone/dist/src/main/compose/ozone/docker-config
index 941969661fd..9ae3bb91514 100644
--- a/hadoop-ozone/dist/src/main/compose/ozone/docker-config
+++ b/hadoop-ozone/dist/src/main/compose/ozone/docker-config
@@ -89,9 +89,19 @@ OZONE-SITE.XML_ozone.snapshot.defrag.service.interval=30s
 # store instead (see ozone-site.xml.template for full setup instructions).
 #
 # To enable the chatbot locally for testing:
-#   1. Uncomment the lines below.
-#   2. Replace YOUR_GEMINI_API_KEY_HERE with a real key.
+#   1. Uncomment ONE block below (direct provider or gateway).
+#   2. Replace the placeholder key with a real key.
 #   3. Never commit a real key to git — rotate it immediately if you do.
+#
+# --- Direct provider (Gemini example; OpenAI and Anthropic are also 
supported) ---
 # OZONE-SITE.XML_ozone.recon.chatbot.enabled=true
 # OZONE-SITE.XML_ozone.recon.chatbot.provider=gemini
 # OZONE-SITE.XML_ozone.recon.chatbot.gemini.api.key=YOUR_GEMINI_API_KEY_HERE
+#
+# --- OpenAI-compatible gateway (LiteLLM etc.) — one door for all models ---
+# OZONE-SITE.XML_ozone.recon.chatbot.enabled=true
+# OZONE-SITE.XML_ozone.recon.chatbot.provider=gateway
+# OZONE-SITE.XML_ozone.recon.chatbot.gateway.base.url=https://your-gateway/v1
+# OZONE-SITE.XML_ozone.recon.chatbot.gateway.api.key=YOUR_GATEWAY_KEY_HERE
+# 
OZONE-SITE.XML_ozone.recon.chatbot.gateway.models=gpt-4.1,claude-sonnet,gemini-2.5-flash
+# OZONE-SITE.XML_ozone.recon.chatbot.default.model=claude-sonnet
diff --git a/hadoop-ozone/dist/src/main/compose/ozonesecure/docker-config 
b/hadoop-ozone/dist/src/main/compose/ozonesecure/docker-config
index c8ef489a600..2fc32beeb97 100644
--- a/hadoop-ozone/dist/src/main/compose/ozonesecure/docker-config
+++ b/hadoop-ozone/dist/src/main/compose/ozonesecure/docker-config
@@ -180,3 +180,29 @@ OZONE-SITE.XML_ozone.om.tenant.dev.skip.ranger=true
 
 # Explicitly enable filesystem snapshot feature for this Docker compose cluster
 OZONE-SITE.XML_ozone.filesystem.snapshot.enabled=true
+
+# Recon AI Chatbot — DISABLED by default.
+#
+# WARNING: The plaintext API key approach shown below is for LOCAL DOCKER
+# TESTING ONLY. It must NOT be used on production clusters because plaintext
+# keys are exposed via 'hadoop conf | grep api.key' and via Recon's /conf HTTP
+# endpoint. For production clusters, store the key in a Hadoop JCEKS credential
+# store instead (see ozone-site.xml.template for full setup instructions).
+#
+# To enable the chatbot locally for testing:
+#   1. Uncomment ONE block below (direct provider or gateway).
+#   2. Replace the placeholder key with a real key.
+#   3. Never commit a real key to git — rotate it immediately if you do.
+#
+# --- Direct provider (Gemini example; OpenAI and Anthropic are also 
supported) ---
+# OZONE-SITE.XML_ozone.recon.chatbot.enabled=true
+# OZONE-SITE.XML_ozone.recon.chatbot.provider=gemini
+# OZONE-SITE.XML_ozone.recon.chatbot.gemini.api.key=YOUR_GEMINI_API_KEY_HERE
+#
+# --- OpenAI-compatible gateway (LiteLLM etc.) — one door for all models ---
+# OZONE-SITE.XML_ozone.recon.chatbot.enabled=true
+# OZONE-SITE.XML_ozone.recon.chatbot.provider=gateway
+# OZONE-SITE.XML_ozone.recon.chatbot.gateway.base.url=https://your-gateway/v1
+# OZONE-SITE.XML_ozone.recon.chatbot.gateway.api.key=YOUR_GATEWAY_KEY_HERE
+# 
OZONE-SITE.XML_ozone.recon.chatbot.gateway.models=gpt-4.1,claude-sonnet,gemini-2.5-flash
+# OZONE-SITE.XML_ozone.recon.chatbot.default.model=claude-sonnet
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/ChatbotConfigKeys.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/ChatbotConfigKeys.java
index 25ba763e133..85d2283524c 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/ChatbotConfigKeys.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/ChatbotConfigKeys.java
@@ -55,6 +55,12 @@ public final class ChatbotConfigKeys {
   public static final String OZONE_RECON_CHATBOT_ANTHROPIC_API_KEY = 
OZONE_RECON_CHATBOT_PREFIX
       + "anthropic.api.key";
 
+  /**
+   * Gateway API key. Used when provider is set to "gateway" to route all 
requests
+   * (regardless of underlying model) through an OpenAI-compatible gateway 
(e.g. LiteLLM).
+   */
+  public static final String OZONE_RECON_CHATBOT_GATEWAY_API_KEY = 
OZONE_RECON_CHATBOT_PREFIX + "gateway.api.key";
+
   // ── Per-provider base URL overrides (optional) ──────────────
   public static final String OZONE_RECON_CHATBOT_OPENAI_BASE_URL = 
OZONE_RECON_CHATBOT_PREFIX + "openai.base.url";
   public static final String OZONE_RECON_CHATBOT_OPENAI_BASE_URL_DEFAULT = 
"https://api.openai.com";;
@@ -62,6 +68,12 @@ public final class ChatbotConfigKeys {
   public static final String OZONE_RECON_CHATBOT_GEMINI_BASE_URL = 
OZONE_RECON_CHATBOT_PREFIX + "gemini.base.url";
   public static final String OZONE_RECON_CHATBOT_GEMINI_BASE_URL_DEFAULT = 
"https://generativelanguage.googleapis.com/v1beta/openai/";;
 
+  /**
+   * Required base URL when using the "gateway" provider. Points to your 
internal
+   * OpenAI-compatible endpoint. No default is provided.
+   */
+  public static final String OZONE_RECON_CHATBOT_GATEWAY_BASE_URL = 
OZONE_RECON_CHATBOT_PREFIX + "gateway.base.url";
+
   // ── Execution policy ────────────────────────────────────────
 
   public static final String OZONE_RECON_CHATBOT_EXEC_REQUIRE_SAFE_SCOPE = 
OZONE_RECON_CHATBOT_PREFIX
@@ -141,6 +153,14 @@ public final class ChatbotConfigKeys {
   public static final String OZONE_RECON_CHATBOT_ANTHROPIC_MODELS_DEFAULT =
       "claude-opus-4-6,claude-sonnet-4-6";
 
+  /**
+   * Comma-separated list of model aliases exposed by your OpenAI-compatible 
gateway.
+   * Required when using the "gateway" provider. Include all models (Claude, 
Gemini, GPT)
+   * that your gateway supports. No default is provided.
+   */
+  public static final String OZONE_RECON_CHATBOT_GATEWAY_MODELS =
+      OZONE_RECON_CHATBOT_PREFIX + "gateway.models";
+
   // ── Anthropic-specific headers ───────────────────────────────
   /**
    * Controls the Anthropic beta feature header sent with every request.
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LangChain4jDispatcher.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LangChain4jDispatcher.java
index b13d9e174b8..2af750eef04 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LangChain4jDispatcher.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/chatbot/llm/LangChain4jDispatcher.java
@@ -79,6 +79,7 @@ public class LangChain4jDispatcher implements LLMClient {
   private static final String PROVIDER_OPENAI = "openai";
   private static final String PROVIDER_GEMINI = "gemini";
   private static final String PROVIDER_ANTHROPIC = "anthropic";
+  private static final String PROVIDER_GATEWAY = "gateway";
 
   private final OzoneConfiguration configuration;
   private final CredentialHelper credentialHelper;
@@ -147,6 +148,12 @@ public LangChain4jDispatcher(OzoneConfiguration 
configuration,
           ChatbotConfigKeys.OZONE_RECON_CHATBOT_ANTHROPIC_MODELS,
           ChatbotConfigKeys.OZONE_RECON_CHATBOT_ANTHROPIC_MODELS_DEFAULT));
     }
+    if (!credentialHelper.getSecret(
+        ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_API_KEY).isEmpty()) {
+      supportedModels.put("gateway", parseModelList(configuration,
+          ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_MODELS,
+          ""));
+    }
 
     this.routing = new LlmRouting(defaultProvider, defaultModel, 
supportedModels);
 
@@ -363,11 +370,31 @@ private ChatLanguageModel buildModelInternal(String 
provider, String model,
       return buildGeminiModel(model, temperature, maxTokens);
     case PROVIDER_ANTHROPIC:
       return buildAnthropicModel(model, temperature, maxTokens);
+    case PROVIDER_GATEWAY:
+      return buildGatewayModel(model, temperature, maxTokens);
     default:
       throw new LLMException("Unknown or unconfigured provider: '" + provider 
+ "'");
     }
   }
 
+  private ChatLanguageModel buildGatewayModel(String model, double 
temperature, int maxTokens)
+      throws LLMException {
+    String key = 
resolveKey(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_API_KEY, "gateway");
+    String baseUrl = 
configuration.get(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_BASE_URL);
+    if (StringUtils.isBlank(baseUrl)) {
+      throw new 
LLMException(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_BASE_URL
+          + " must be set when using the gateway provider.");
+    }
+
+    OpenAiChatModel.OpenAiChatModelBuilder builder = OpenAiChatModel.builder()
+        .apiKey(key)
+        .modelName(model)
+        .baseUrl(baseUrl)
+        .timeout(timeout);
+    applyGenerationParams(builder, temperature, maxTokens);
+    return builder.build();
+  }
+
   private ChatLanguageModel buildOpenAiModel(String model, double temperature, 
int maxTokens)
       throws LLMException {
     String key = 
resolveKey(ChatbotConfigKeys.OZONE_RECON_CHATBOT_OPENAI_API_KEY, "openai");
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLangChain4jDispatcher.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLangChain4jDispatcher.java
index 1e253fbad3b..fec1b73c533 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLangChain4jDispatcher.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLangChain4jDispatcher.java
@@ -75,6 +75,13 @@ public void testIsAvailableWithGeminiKey() {
     assertTrue(dispatcher.isAvailable());
   }
 
+  @Test
+  public void testIsAvailableWithGatewayKey() {
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_API_KEY, 
"fake-gateway-key");
+    dispatcher = new LangChain4jDispatcher(conf, new CredentialHelper(conf));
+    assertTrue(dispatcher.isAvailable());
+  }
+
   @Test
   public void testGetSupportedModelsEmptyWithoutKeys() {
     List<String> models = dispatcher.getSupportedModels();
@@ -91,6 +98,17 @@ public void testGetSupportedModelsWithGeminiKey() {
     assertTrue(models.stream().anyMatch(m -> m.startsWith("gemini")));
   }
 
+  @Test
+  public void testGetSupportedModelsWithGatewayKey() {
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_API_KEY, 
"fake-gateway-key");
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_MODELS, 
"claude-sonnet,gpt-4.1");
+    dispatcher = new LangChain4jDispatcher(conf, new CredentialHelper(conf));
+    List<String> models = dispatcher.getSupportedModels();
+    assertFalse(models.isEmpty());
+    assertTrue(models.contains("claude-sonnet"));
+    assertTrue(models.contains("gpt-4.1"));
+  }
+
   @Test
   public void testUnknownModelFallsBackToDefaultProvider() {
     List<LLMClient.ChatMessage> messages = new ArrayList<>();
@@ -166,4 +184,43 @@ public void 
testMismatchedProviderAndModelFallsBackToDefaults() {
     assertTrue(ex.getMessage().toLowerCase().contains("gemini"),
         "Mismatched pair should fall back to default gemini provider");
   }
+
+  @Test
+  public void testGatewayProviderRoutesCorrectly() {
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_API_KEY, 
"fake-gateway-key");
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_BASE_URL, 
"https://gateway.example.com";);
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_MODELS, 
"claude-sonnet");
+    dispatcher = new LangChain4jDispatcher(conf, new CredentialHelper(conf));
+
+    List<LLMClient.ChatMessage> messages = new ArrayList<>();
+    messages.add(new LLMClient.ChatMessage("user", "hello"));
+
+    // Fake URL is not reachable; routing past key/base-url validation means 
we attempted gateway.
+    Exception ex = assertThrows(Exception.class, () ->
+        dispatcher.chatCompletion(messages, "claude-sonnet", "gateway",
+            new GenParams(0.1, 1000), null));
+    String msg = ex.getMessage() != null ? ex.getMessage() : "";
+    assertFalse(msg.contains("must be set when using the gateway provider"),
+        "Should not fail on missing gateway.base.url");
+    assertFalse(msg.contains("No API key configured"),
+        "Should not fail on missing gateway API key");
+  }
+
+  @Test
+  public void testGatewayProviderThrowsWhenBaseUrlBlank() {
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_API_KEY, 
"fake-gateway-key");
+    conf.set(ChatbotConfigKeys.OZONE_RECON_CHATBOT_GATEWAY_MODELS, 
"claude-sonnet");
+    // Leave base URL blank
+    dispatcher = new LangChain4jDispatcher(conf, new CredentialHelper(conf));
+
+    List<LLMClient.ChatMessage> messages = new ArrayList<>();
+    messages.add(new LLMClient.ChatMessage("user", "hello"));
+
+    LLMClient.LLMException ex = assertThrows(LLMClient.LLMException.class, () 
->
+        dispatcher.chatCompletion(messages, "claude-sonnet", "gateway",
+            new GenParams(0.1, 1000), null));
+
+    assertTrue(ex.getMessage().contains("must be set when using the gateway 
provider"),
+        "Should throw if gateway.base.url is missing");
+  }
 }
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLlmRouting.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLlmRouting.java
index 270eb2bb626..1e772213d2a 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLlmRouting.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/chatbot/llm/TestLlmRouting.java
@@ -39,6 +39,7 @@ public void setUp() {
     Map<String, List<String>> supportedModels = new HashMap<>();
     supportedModels.put("gemini", Arrays.asList("gemini-2.5-flash", 
"gemini-2.5-pro"));
     supportedModels.put("openai", Arrays.asList("gpt-4.1", "gpt-4.1-mini"));
+    supportedModels.put("gateway", Arrays.asList("claude-sonnet", 
"gpt-4.1-gateway"));
     routing = new LlmRouting(DEFAULT_PROVIDER, DEFAULT_MODEL, supportedModels);
   }
 
@@ -49,6 +50,13 @@ public void testValidProviderAndModel() {
     assertEquals("gpt-4.1", resolved.getModel());
   }
 
+  @Test
+  public void testValidGatewayProviderAndModel() {
+    LlmRouting.Resolved resolved = routing.resolve("gateway", "claude-sonnet");
+    assertEquals("gateway", resolved.getProvider());
+    assertEquals("claude-sonnet", resolved.getModel());
+  }
+
   @Test
   public void testSupportedModelInfersProviderWhenProviderMissing() {
     LlmRouting.Resolved resolved = routing.resolve(null, "gpt-4.1");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to