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

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new e570d5c98d Fixes #8336 : Upgrade the Language Model Chat transform to 
langchain4j 1.20.0 (#8341)
e570d5c98d is described below

commit e570d5c98da6c1b99200c6d98b3afbba82299cda
Author: Bart Maertens <[email protected]>
AuthorDate: Sat Sep 12 13:31:09 2026 +0200

    Fixes #8336 : Upgrade the Language Model Chat transform to langchain4j 
1.20.0 (#8341)
    
    Move from langchain4j 0.29.1 to 1.20.0: ChatLanguageModel becomes ChatModel,
    generate() becomes chat() returning ChatResponse, and the removed 
ChatMessage.text()
    is replaced by a helper that collects the text parts of a multimodal 
message.
    OpenAI proxy support moves to the JDK http client builder, and the free 
format
    string of Ollama and Mistral is mapped onto a ResponseFormat.
    
    The Hugging Face integration no longer uses langchain4j-hugging-face, a beta
    artifact that upstream deprecated for removal. The four vendored classes 
under
    dev.langchain4j are replaced by a client in the plugin's own package, 
calling the
    API over the langchain4j http client. This drops retrofit, okhttp-sse, gson 
and
    the abandoned openai4j from the distribution.
    
    Api keys and access tokens are stored as passwords and masked in the 
dialog, the
    way Hop stores other secrets. Values holding variables are still written as 
they
    stand. The openAiApiKey field default lost a stray brace it carried since 
the
    transform was contributed.
    
    Adds a stub server test covering all five providers and an Ollama 
integration
    test project, disabled by default because it pulls a model.
---
 assemblies/static/src/main/resources/LICENSE       |  35 ++-
 .../integration-tests-languagemodelchat.yaml       |  48 ++++
 .../pipeline/transforms/languagemodelchat.adoc     |   8 +-
 .../languagemodelchat/0001-ollama-chat.hpl         | 311 +++++++++++++++++++++
 integration-tests/languagemodelchat/README.md      |  57 ++++
 .../languagemodelchat/dev-env-config.json          |  14 +
 integration-tests/languagemodelchat/disabled.txt   |  21 ++
 .../languagemodelchat/hop-config.json              | 290 +++++++++++++++++++
 .../languagemodelchat/main-0001-ollama-chat.hwf    |  89 ++++++
 .../metadata/pipeline-run-configuration/local.json |  17 ++
 .../metadata/workflow-run-configuration/local.json |   9 +
 .../languagemodelchat/project-config.json          |  15 +
 plugins/transforms/languagemodelchat/pom.xml       |  65 +----
 .../DedicatedEndpointFactoryCreator.java           |  46 ---
 .../DedicatedEndpointHuggingFaceApi.java           |  38 ---
 .../DedicatedEndpointHuggingFaceChatModel.java     | 178 ------------
 .../DedicatedEndpointHuggingFaceClient.java        | 123 --------
 .../languagemodelchat/LanguageModelChat.java       |   9 +-
 .../languagemodelchat/LanguageModelChatMeta.java   |  48 ++--
 .../languagemodelchat/internals/ChatMessages.java  |  57 ++++
 .../internals/LanguageModelChatModelName.java      |  43 +--
 .../internals/LanguageModelFacade.java             |  99 +++----
 .../huggingface/HuggingFaceChatModel.java          | 139 +++++++++
 .../internals/huggingface/HuggingFaceClient.java   | 146 ++++++++++
 .../ui/models/AbstractModelComposite.java          |   6 +
 .../internals/ui/models/AnthropicComposite.java    |   2 +-
 .../internals/ui/models/HuggingFaceComposite.java  |   2 +-
 .../internals/ui/models/MistralComposite.java      |   2 +-
 .../internals/ui/models/OpenAiComposite.java       |   2 +-
 .../fact-checking/bespoke-minicheck.hpl            |   4 +-
 .../languagemodelchat/zero-shot/anthropic.hpl      |   2 +-
 .../languagemodelchat/zero-shot/huggingface.hpl    |   2 +-
 .../languagemodelchat/zero-shot/openai.hpl         |   2 +-
 .../LanguageModelChatMetaSecretsTest.java          | 117 ++++++++
 .../internals/LanguageModelFacadeChatTest.java     | 211 ++++++++++++++
 35 files changed, 1692 insertions(+), 565 deletions(-)

diff --git a/assemblies/static/src/main/resources/LICENSE 
b/assemblies/static/src/main/resources/LICENSE
index aa95690231..b45ccefa5c 100644
--- a/assemblies/static/src/main/resources/LICENSE
+++ b/assemblies/static/src/main/resources/LICENSE
@@ -766,9 +766,6 @@ This product bundles conscrypt-openjdk-uber 2.5.2, which is 
available under a
 This product bundles content-type 2.3, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles converter-gson 2.9.0, which is available under a
-"Apache 2.0" license. For details, see LICENSE.
-
 This product bundles converter-jackson 2.7.2, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
@@ -1885,7 +1882,7 @@ This product bundles jsr305 3.0.2, which is available 
under a
 This product bundles jt400 21.0.7, which is available under a
 "IBM Public 1.0" license. For details, see licenses/LICENSE-IBM10.
 
-This product bundles jtokkit 1.0.0, which is available under a
+This product bundles jtokkit 1.1.0, which is available under a
 "The MIT" license. For details, see licenses/LICENSE-MIT.
 
 This product bundles jts-core 1.20.0, which is available under a
@@ -2008,22 +2005,28 @@ This product bundles kryo-shaded 4.0.3, which is 
available under a
 This product bundles lang-tag 1.7, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles langchain4j-anthropic 0.29.1, which is available under a
+This product bundles langchain4j-anthropic 1.20.0, which is available under a
+"Apache 2.0" license. For details, see LICENSE.
+
+This product bundles langchain4j-core 1.20.0, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles langchain4j-core 0.29.1, which is available under a
+This product bundles langchain4j-http-client 1.20.0, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles langchain4j-hugging-face 0.29.1, which is available under 
a
+This product bundles langchain4j-http-client-jdk 1.20.0, which is available 
under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles langchain4j-mistral-ai 0.29.1, which is available under a
+This product bundles langchain4j-mistral-ai 1.20.0, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles langchain4j-ollama 0.29.1, which is available under a
+This product bundles langchain4j-ollama 1.20.0, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles langchain4j-open-ai 0.29.1, which is available under a
+This product bundles langchain4j-open-ai 1.20.0, which is available under a
+"Apache 2.0" license. For details, see LICENSE.
+
+This product bundles langchain4j-reactive-streaming 1.20.0-beta30, which is 
available under a
 "Apache 2.0" license. For details, see LICENSE.
 
 This product bundles LatencyUtils 2.0.3, which is available under a
@@ -2218,6 +2221,9 @@ This product bundles mvel2 2.5.2.Final, which is 
available under a
 This product bundles mxdump 0.14, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
+This product bundles mutiny-zero 1.3.1, which is available under a
+"Apache 2.0" license. For details, see LICENSE.
+
 This product bundles native-protocol 1.5.1, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
@@ -2425,9 +2431,6 @@ This product bundles ohc-core-j8 0.5.1, which is 
available under a
 This product bundles okhttp 4.12.0, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles okhttp-sse 4.12.0, which is available under a
-"Apache 2.0" license. For details, see LICENSE.
-
 This product bundles okhttp-urlconnection 3.12.2, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
@@ -2443,9 +2446,6 @@ This product bundles okio-fakefilesystem-jvm 3.4.0, which 
is available under a
 This product bundles okio-jvm 3.18.1, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles openai4j 0.17.0, which is available under a
-"Apache 2.0" license. For details, see LICENSE.
-
 This product bundles opencensus-api 0.31.1, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
@@ -2800,9 +2800,6 @@ This product bundles reporter-config3 3.0.3, which is 
available under a
 This product bundles retrofit 2.7.2, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
-This product bundles retrofit 2.9.0, which is available under a
-"Apache 2.0" license. For details, see LICENSE.
-
 This product bundles rgxgen 3.1, which is available under a
 "Apache 2.0" license. For details, see LICENSE.
 
diff --git a/docker/integration-tests/integration-tests-languagemodelchat.yaml 
b/docker/integration-tests/integration-tests-languagemodelchat.yaml
new file mode 100644
index 0000000000..7a1f8f6c81
--- /dev/null
+++ b/docker/integration-tests/integration-tests-languagemodelchat.yaml
@@ -0,0 +1,48 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+services:
+  integration_test_languagemodelchat:
+    extends:
+      file: integration-tests-base.yaml
+      service: integration_test
+    depends_on:
+      ollama:
+        condition: service_healthy
+    links:
+      - ollama
+
+  ollama:
+    # Pin version for CI stability; tests reach it over compose DNS 
(ollama:11434).
+    image: ollama/ollama:0.34.0
+    hostname: ollama
+    # The image ships no models, so the server is started and the test model 
pulled here. The
+    # healthcheck below is what holds the test container back until that pull 
has finished.
+    entrypoint: ["/bin/sh", "-c"]
+    command:
+      - |
+        ollama serve &
+        until ollama list >/dev/null 2>&1; do sleep 1; done
+        ollama pull smollm2:135m
+        wait
+    healthcheck:
+      # smollm2:135m is ~270MB: small enough to pull on every run, large 
enough to answer a prompt.
+      test: ["CMD-SHELL", "ollama list | grep -q smollm2"]
+      interval: 10s
+      timeout: 10s
+      retries: 60
+      start_period: 20s
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc
index be5aa622c1..7517380483 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc
@@ -121,7 +121,7 @@ Below is a table detailing parameters specific to 
individual language model APIs
 |Option |Description
 
 |API/Endpoint URL
-|Specifies the identifier (e.g., URL, model ID, or image ID) used to access 
and interact with models provided by services like OpenAI, Anthropic, Mistral, 
Ollama, or Hugging Face.
+|Specifies the identifier (e.g., URL, model ID, or image ID) used to access 
and interact with models provided by services like OpenAI, Anthropic, Mistral, 
Ollama, or Hugging Face. For Hugging Face, a model id is sent to the Hugging 
Face inference router, a URL is called as a dedicated inference endpoint.
 
 |Temperature
 |Controls the randomness of the output: high values lead to more diverse text 
(creative/inventive), while low values produce more focused and deterministic 
responses. The value typically ranges between 0 and 1 but can exceed this range 
depending on the model. Note: Unless intentional, it is generally best to 
adjust only one parameter; either temperature or Top-P/K, rather than multiple 
simultaneously.
@@ -136,7 +136,9 @@ Below is a table detailing parameters specific to 
individual language model APIs
 |Determines how long to wait for a response before giving up.
 
 |API Key/AccessToken
-| Unique access value used to authenticate and authorise access to the 
endpoint service.
+| Unique access value used to authenticate and authorise access to the 
endpoint service. The value
+is masked in the dialog and stored encrypted in the pipeline, unless it is a 
variable such as
+`'${OPENAI_API_KEY}'`, which is stored as it stands so it can be resolved at 
run time.
 
 |Model Name
 |Identifies which specific model to use.
@@ -148,7 +150,7 @@ Below is a table detailing parameters specific to 
individual language model APIs
 |The maximum number of retry attempts for failed API requests.
 
 |Response Format
-|Defines the structure used for the model's responses. Note: While some APIs 
support this option, not all models are compatible with it. Additionally, the 
API may require explicitly instructing the model to produce JSON to ensure 
proper functionality.
+|Defines the structure used for the model's responses. A value mentioning json 
asks the model for JSON, any other value asks for text. Note: While some APIs 
support this option, not all models are compatible with it. Additionally, the 
API may require explicitly instructing the model to produce JSON to ensure 
proper functionality.
 
 |Seed
 |Attempts to sample deterministically, ensuring consistent results for 
repeated requests with the same seed, prompt, and parameters.
diff --git a/integration-tests/languagemodelchat/0001-ollama-chat.hpl 
b/integration-tests/languagemodelchat/0001-ollama-chat.hpl
new file mode 100644
index 0000000000..d13267aee2
--- /dev/null
+++ b/integration-tests/languagemodelchat/0001-ollama-chat.hpl
@@ -0,0 +1,311 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0001-ollama-chat</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Send a prompt to Ollama and check the answer and the token 
counts come back</description>
+    <extended_description>A small model says what it likes, so the checks are 
on the shape of the
+      output: no error, an answer, and token counts the transform could only 
have read from a real
+      response.</extended_description>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters/>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/09/11 20:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/09/11 20:00:00.000</modified_date>
+  </info>
+  <notepads/>
+  <order>
+    <hop>
+      <from>prompt</from>
+      <to>Language Model Chat</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Language Model Chat</from>
+      <to>check no error</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check no error</from>
+      <to>check answer</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check no error</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check answer</from>
+      <to>check token counts</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check answer</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check token counts</from>
+      <to>answer</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>check token counts</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>prompt</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>prompt</name>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <data>
+      <line>
+        <item>Reply with one word: what colour is a clear sky at noon?</item>
+      </line>
+    </data>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Language Model Chat</name>
+    <type>LanguageModelChat</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <inputField>prompt</inputField>
+    <inputChatJson>false</inputChatJson>
+    <outputChatJson>false</outputChatJson>
+    <mock>false</mock>
+    <outputFieldNamePrefix>llm_</outputFieldNamePrefix>
+    <modelType>OLLAMA</modelType>
+    <parallelism>1</parallelism>
+    <openAiUseProxy>false</openAiUseProxy>
+    <openAiLogRequests>false</openAiLogRequests>
+    <openAiLogResponses>false</openAiLogResponses>
+    <huggingFaceReturnFullText>false</huggingFaceReturnFullText>
+    <huggingFaceWaitForModel>false</huggingFaceWaitForModel>
+    <mistralSafePrompt>false</mistralSafePrompt>
+    <mistralLogRequests>false</mistralLogRequests>
+    <mistralLogResponses>false</mistralLogResponses>
+    <ollamaImageEndpoint>${OLLAMA_BASE_URL}</ollamaImageEndpoint>
+    <ollamaModelName>${OLLAMA_MODEL}</ollamaModelName>
+    <ollamaTemperature>0.1</ollamaTemperature>
+    <ollamaNumPredict>32</ollamaNumPredict>
+    <ollamaTimeout>300</ollamaTimeout>
+    <ollamaMaxRetries>3</ollamaMaxRetries>
+    <anthropicLogRequests>false</anthropicLogRequests>
+    <anthropicLogResponses>false</anthropicLogResponses>
+    <attributes/>
+    <GUI>
+      <xloc>256</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check no error</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to>check answer</send_true_to>
+    <send_false_to>Abort</send_false_to>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <leftvalue>llm_finish_reason</leftvalue>
+        <function>&lt;&gt;</function>
+        <rightvalue/>
+        <value>
+          <name>constant</name>
+          <type>String</type>
+          <text>ERROR</text>
+          <length>-1</length>
+          <precision>-1</precision>
+          <isnull>N</isnull>
+          <mask>####0;-####0</mask>
+        </value>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>432</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check answer</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to>check token counts</send_true_to>
+    <send_false_to>Abort</send_false_to>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <leftvalue>llm_output</leftvalue>
+        <function>IS NOT NULL</function>
+        <rightvalue/>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>608</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check token counts</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to>answer</send_true_to>
+    <send_false_to>Abort</send_false_to>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <leftvalue>llm_total_token_count</leftvalue>
+        <function>&gt;</function>
+        <rightvalue/>
+        <value>
+          <name>constant</name>
+          <type>Integer</type>
+          <text>0</text>
+          <length>-1</length>
+          <precision>-1</precision>
+          <isnull>N</isnull>
+          <mask>####0;-####0</mask>
+        </value>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>784</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>answer</name>
+    <type>WriteToLog</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <loglevel>log_level_basic</loglevel>
+    <displayHeader>Y</displayHeader>
+    <limitRows>Y</limitRows>
+    <limitRowsNumber>1</limitRowsNumber>
+    <logmessage>Ollama answered</logmessage>
+    <fields>
+      <field>
+        <name>llm_model_name</name>
+      </field>
+      <field>
+        <name>llm_finish_reason</name>
+      </field>
+      <field>
+        <name>llm_total_token_count</name>
+      </field>
+      <field>
+        <name>llm_output</name>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>960</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Abort</name>
+    <type>Abort</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <row_threshold>0</row_threshold>
+    <message>The Language Model Chat transform did not return a usable answer 
from Ollama</message>
+    <always_log_rows>Y</always_log_rows>
+    <abort_option>ABORT_WITH_ERROR</abort_option>
+    <attributes/>
+    <GUI>
+      <xloc>608</xloc>
+      <yloc>240</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling/>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/languagemodelchat/README.md 
b/integration-tests/languagemodelchat/README.md
new file mode 100644
index 0000000000..16cf4a15ec
--- /dev/null
+++ b/integration-tests/languagemodelchat/README.md
@@ -0,0 +1,57 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Language Model Chat integration tests
+
+The Language Model Chat transform talks to five providers, four of which need
+an API key. Ollama does not, so it is the one that can be tested here: a real
+model answers a real prompt over HTTP, which covers the request the transform
+builds, the call itself and the response it reads back.
+
+## Running
+
+The project carries a `disabled.txt`: `ollama/ollama` is a large image and the
+model still has to be pulled on top of it, which the standard suite should not
+pay for on every run. Run it explicitly:
+
+```
+./run-tests-docker.sh PROJECT_NAME=languagemodelchat INCLUDE_DISABLED=true
+```
+
+`docker/integration-tests/integration-tests-languagemodelchat.yaml` starts
+Ollama, pulls `smollm2:135m` (~270MB) and holds the test container back until
+the pull has finished. The endpoint and the model are set in
+`dev-env-config.json`, so a local Ollama can be used instead:
+
+```
+OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_MODEL=phi3
+```
+
+## What is checked
+
+A 135M parameter model writes what it likes, so the answer itself is not
+compared against anything. The checks are on the shape of the result:
+
+- `llm_finish_reason` is not `ERROR`. The transform catches every failure from
+  the model call and writes `ERROR` into this field rather than failing the
+  row, so without this check a pipeline that reached no model at all still ends
+  green.
+- `llm_output` is not null.
+- `llm_total_token_count` is greater than zero, which the transform can only
+  have read from a real response.
+
+Any of the three failing routes the row to `Abort`, which fails the pipeline.
diff --git a/integration-tests/languagemodelchat/dev-env-config.json 
b/integration-tests/languagemodelchat/dev-env-config.json
new file mode 100644
index 0000000000..1f3645221b
--- /dev/null
+++ b/integration-tests/languagemodelchat/dev-env-config.json
@@ -0,0 +1,14 @@
+{
+  "variables": [
+    {
+      "name": "OLLAMA_BASE_URL",
+      "value": "http://ollama:11434";,
+      "description": "Ollama endpoint on the docker integration-test network"
+    },
+    {
+      "name": "OLLAMA_MODEL",
+      "value": "smollm2:135m",
+      "description": "Model pulled by the ollama service in 
docker/integration-tests/integration-tests-languagemodelchat.yaml"
+    }
+  ]
+}
diff --git a/integration-tests/languagemodelchat/disabled.txt 
b/integration-tests/languagemodelchat/disabled.txt
new file mode 100644
index 0000000000..25ec6b4d5c
--- /dev/null
+++ b/integration-tests/languagemodelchat/disabled.txt
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Needs the ollama image and a model pull, which the standard suite should not 
pay for on every
+# run. Enable it explicitly:
+#
+#   ./run-tests-docker.sh PROJECT_NAME=languagemodelchat INCLUDE_DISABLED=true
+#
diff --git a/integration-tests/languagemodelchat/hop-config.json 
b/integration-tests/languagemodelchat/hop-config.json
new file mode 100644
index 0000000000..d9e1e6562e
--- /dev/null
+++ b/integration-tests/languagemodelchat/hop-config.json
@@ -0,0 +1,290 @@
+{
+  "variables": [
+    {
+      "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION",
+      "value": "N",
+      "description": "System wide flag to allow lenient string to number 
conversion for backward compatibility. If this setting is set to \"Y\", an 
string starting with digits will be converted successfully into a number. 
(example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending 
on the decimal and grouping symbol). The default (N) will be to throw an error 
if non-numeric symbols are found in the string."
+    },
+    {
+      "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE",
+      "value": "N",
+      "description": "System wide flag to ignore timezone while writing 
date/timestamp value to the database."
+    },
+    {
+      "name": "HOP_LOG_SIZE_LIMIT",
+      "value": "0",
+      "description": "The log size limit for all pipelines and workflows that 
don't have the \"log size limit\" property set in their respective properties."
+    },
+    {
+      "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL",
+      "value": "N",
+      "description": "NULL vs Empty String. If this setting is set to Y, an 
empty string and null are different. Otherwise they are not."
+    },
+    {
+      "name": "HOP_MAX_LOG_SIZE_IN_LINES",
+      "value": "0",
+      "description": "The maximum number of log lines that are kept internally 
by Hop. Set to 0 to keep all rows (default)"
+    },
+    {
+      "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES",
+      "value": "1440",
+      "description": "The maximum age (in minutes) of a log line while being 
kept internally by Hop. Set to 0 to keep all rows indefinitely (default)"
+    },
+    {
+      "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE",
+      "value": "5000",
+      "description": "The maximum number of workflow trackers kept in memory"
+    },
+    {
+      "name": "HOP_MAX_ACTIONS_LOGGED",
+      "value": "5000",
+      "description": "The maximum number of action results kept in memory for 
logging purposes."
+    },
+    {
+      "name": "HOP_MAX_LOGGING_REGISTRY_SIZE",
+      "value": "10000",
+      "description": "The maximum number of logging registry entries kept in 
memory for logging purposes."
+    },
+    {
+      "name": "HOP_LOG_TAB_REFRESH_DELAY",
+      "value": "1000",
+      "description": "The hop log tab refresh delay."
+    },
+    {
+      "name": "HOP_LOG_TAB_REFRESH_PERIOD",
+      "value": "1000",
+      "description": "The hop log tab refresh period."
+    },
+    {
+      "name": "HOP_PLUGIN_CLASSES",
+      "value": null,
+      "description": "A comma delimited list of classes to scan for plugin 
annotations"
+    },
+    {
+      "name": "HOP_PLUGIN_PACKAGES",
+      "value": null,
+      "description": "A comma delimited list of packages to scan for plugin 
annotations (warning: slow!!)"
+    },
+    {
+      "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT",
+      "value": "0",
+      "description": "The maximum number of transform performance snapshots to 
keep in memory. Set to 0 to keep all snapshots indefinitely (default)"
+    },
+    {
+      "name": "HOP_ROWSET_GET_TIMEOUT",
+      "value": "50",
+      "description": "The name of the variable that optionally contains an 
alternative rowset get timeout (in ms). This only makes a difference for 
extremely short lived pipelines."
+    },
+    {
+      "name": "HOP_ROWSET_PUT_TIMEOUT",
+      "value": "50",
+      "description": "The name of the variable that optionally contains an 
alternative rowset put timeout (in ms). This only makes a difference for 
extremely short lived pipelines."
+    },
+    {
+      "name": "HOP_CORE_TRANSFORMS_FILE",
+      "value": null,
+      "description": "The name of the project variable that will contain the 
alternative location of the hop-transforms.xml file. You can use this to 
customize the list of available internal transforms outside of the codebase."
+    },
+    {
+      "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE",
+      "value": null,
+      "description": "The name of the project variable that will contain the 
alternative location of the hop-workflow-actions.xml file."
+    },
+    {
+      "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES",
+      "value": "1440",
+      "description": "This project variable will set a time-out after which 
waiting, completed or stopped pipelines and workflows will be automatically 
cleaned up. The default value is 1440 (one day)."
+    },
+    {
+      "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE",
+      "value": null,
+      "description": "Set this variable to an integer that will be returned as 
the Pan JVM exit code."
+    },
+    {
+      "name": "HOP_DISABLE_CONSOLE_LOGGING",
+      "value": "N",
+      "description": "Set this variable to Y to disable standard Hop logging 
to the console. (stdout)"
+    },
+    {
+      "name": "HOP_REDIRECT_STDERR",
+      "value": "N",
+      "description": "Set this variable to Y to redirect stderr to Hop 
logging."
+    },
+    {
+      "name": "HOP_REDIRECT_STDOUT",
+      "value": "N",
+      "description": "Set this variable to Y to redirect stdout to Hop 
logging."
+    },
+    {
+      "name": "HOP_DEFAULT_NUMBER_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default number format"
+    },
+    {
+      "name": "HOP_DEFAULT_BIGNUMBER_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default bignumber format"
+    },
+    {
+      "name": "HOP_DEFAULT_INTEGER_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default integer format"
+    },
+    {
+      "name": "HOP_DEFAULT_DATE_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default date format"
+    },
+    {
+      "name": "HOP_DEFAULT_TIMESTAMP_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default timestamp format"
+    },
+    {
+      "name": "HOP_DEFAULT_SERVLET_ENCODING",
+      "value": null,
+      "description": "Defines the default encoding for servlets, leave it 
empty to use Java default encoding"
+    },
+    {
+      "name": "HOP_FAIL_ON_LOGGING_ERROR",
+      "value": "N",
+      "description": "Set this variable to Y when you want the 
workflow/pipeline fail with an error when the related logging process (e.g. to 
a database) fails."
+    },
+    {
+      "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED",
+      "value": "N",
+      "description": "Set this variable to Y to set the minimum to NULL if 
NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN 
aggregate and MIN is set to the minimum value that is not NULL. See also the 
variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO."
+    },
+    {
+      "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO",
+      "value": "N",
+      "description": "Set this variable to Y to return 0 when all values 
within an aggregate are NULL. Otherwise by default a NULL is returned when all 
values are NULL."
+    },
+    {
+      "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER",
+      "value": "N",
+      "description": "Set this variable to Y for backward compatibility for 
the Text File Output transform. Setting this to Ywill add no header row at all 
when the append option is enabled, regardless if the file is existing or not."
+    },
+    {
+      "name": "HOP_PASSWORD_ENCODER_PLUGIN",
+      "value": "Hop",
+      "description": "Specifies the password encoder plugin to use by ID (Hop 
is the default)."
+    },
+    {
+      "name": "HOP_SYSTEM_HOSTNAME",
+      "value": null,
+      "description": "You can use this variable to speed up hostname lookup. 
Hostname lookup is performed by Hop so that it is capable of logging the server 
on which a workflow or pipeline is executed."
+    },
+    {
+      "name": "HOP_SERVER_JETTY_ACCEPTORS",
+      "value": null,
+      "description": "A variable to configure jetty option: acceptors for 
Carte"
+    },
+    {
+      "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE",
+      "value": null,
+      "description": "A variable to configure jetty option: acceptQueueSize 
for Carte"
+    },
+    {
+      "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME",
+      "value": null,
+      "description": "A variable to configure jetty option: 
lowResourcesMaxIdleTime for Carte"
+    },
+    {
+      "name": 
"HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL",
+      "value": "N",
+      "description": "Set this variable to Y for backward compatibility for 
the Merge Rows (diff) transform. Setting this to Y will use the data from the 
reference stream (instead of the comparison stream) in case the compared rows 
are identical."
+    },
+    {
+      "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE",
+      "value": "false",
+      "description": "Set this variable to false to preserve enclosure symbol 
after splitting the string in the Split fields transform. Changing it to true 
will remove first and last enclosure symbol from the resulting string chunks."
+    },
+    {
+      "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES",
+      "value": "false",
+      "description": "Set this variable to TRUE to allow your pipeline to pass 
'null' fields and/or empty types."
+    },
+    {
+      "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT",
+      "value": "false",
+      "description": "Set this variable to false to preserve global log 
variables defined in pipeline / workflow Properties -> Log panel. Changing it 
to true will clear it when export pipeline / workflow."
+    },
+    {
+      "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT",
+      "value": "1024",
+      "description": "This project variable is used by the Text File Output 
transform. It defines the max number of simultaneously open files within the 
transform. The transform will close/reopen files as necessary to insure the max 
is not exceeded"
+    },
+    {
+      "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE",
+      "value": "0",
+      "description": "This project variable is used by the Text File Output 
transform. It defines the max number of milliseconds between flushes of files 
opened by the transform."
+    },
+    {
+      "name": "HOP_USE_NATIVE_FILE_DIALOG",
+      "value": "N",
+      "description": "Set this value to Y if you want to use the system file 
open/save dialog when browsing files"
+    },
+    {
+      "name": "HOP_AUTO_CREATE_CONFIG",
+      "value": "Y",
+      "description": "Set this value to N if you don't want to automatically 
create a hop configuration file (hop-config.json) when it's missing"
+    }
+  ],
+  "LocaleDefault": "en_BE",
+  "guiProperties": {
+    "FontFixedSize": "13",
+    "MaxUndo": "100",
+    "DarkMode": "Y",
+    "FontNoteSize": "13",
+    "ShowOSLook": "Y",
+    "FontFixedStyle": "0",
+    "FontNoteName": ".AppleSystemUIFont",
+    "FontFixedName": "Monospaced",
+    "FontGraphStyle": "0",
+    "FontDefaultSize": "13",
+    "GraphColorR": "255",
+    "FontGraphSize": "13",
+    "IconSize": "32",
+    "BackgroundColorB": "255",
+    "FontNoteStyle": "0",
+    "FontGraphName": ".AppleSystemUIFont",
+    "FontDefaultName": ".AppleSystemUIFont",
+    "GraphColorG": "255",
+    "UseGlobalFileBookmarks": "Y",
+    "FontDefaultStyle": "0",
+    "GraphColorB": "255",
+    "BackgroundColorR": "255",
+    "BackgroundColorG": "255",
+    "WorkflowDialogStyle": "RESIZE,MAX,MIN",
+    "LineWidth": "1",
+    "ContextDialogShowCategories": "Y"
+  },
+  "projectsConfig": {
+    "enabled": true,
+    "projectMandatory": true,
+    "environmentMandatory": false,
+    "defaultProject": "default",
+    "defaultEnvironment": null,
+    "standardParentProject": "default",
+    "standardProjectsFolder": null,
+    "projectConfigurations": [
+      {
+        "projectName": "default",
+        "projectHome": "${HOP_CONFIG_FOLDER}",
+        "configFilename": "project-config.json"
+      }
+    ],
+    "lifecycleEnvironments": [
+      {
+        "name": "dev",
+        "purpose": "Testing",
+        "projectName": "default",
+        "configurationFiles": [
+          "${PROJECT_HOME}/dev-env-config.json"
+        ]
+      }
+    ],
+    "projectLifecycles": []
+  }
+}
\ No newline at end of file
diff --git a/integration-tests/languagemodelchat/main-0001-ollama-chat.hwf 
b/integration-tests/languagemodelchat/main-0001-ollama-chat.hwf
new file mode 100644
index 0000000000..548396a06a
--- /dev/null
+++ b/integration-tests/languagemodelchat/main-0001-ollama-chat.hwf
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0001-ollama-chat</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description>Run a prompt through the Language Model Chat transform against 
Ollama</description>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/11 20:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/11 20:00:00.000</modified_date>
+  <parameters/>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>64</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0001-ollama-chat.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <clear_files>N</clear_files>
+      <clear_rows>N</clear_rows>
+      <create_parent_folder>N</create_parent_folder>
+      <exec_per_row>N</exec_per_row>
+      <filename>${PROJECT_HOME}/0001-ollama-chat.hpl</filename>
+      <loglevel>Basic</loglevel>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <params_from_previous>N</params_from_previous>
+      <run_configuration>local</run_configuration>
+      <set_append_logfile>N</set_append_logfile>
+      <set_logfile>N</set_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0001-ollama-chat.hpl</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads/>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/languagemodelchat/metadata/pipeline-run-configuration/local.json
 
b/integration-tests/languagemodelchat/metadata/pipeline-run-configuration/local.json
new file mode 100644
index 0000000000..d8e37459d8
--- /dev/null
+++ 
b/integration-tests/languagemodelchat/metadata/pipeline-run-configuration/local.json
@@ -0,0 +1,17 @@
+{
+  "engineRunConfiguration": {
+    "Local": {
+      "feedback_size": "50000",
+      "sample_size": "100",
+      "sample_type_in_gui": "Last",
+      "rowset_size": "10000",
+      "safe_mode": false,
+      "show_feedback": false,
+      "topo_sort": false,
+      "gather_metrics": false
+    }
+  },
+  "configurationVariables": [],
+  "name": "local",
+  "description": "Runs your pipelines locally with the standard local Hop 
pipeline engine"
+}
diff --git 
a/integration-tests/languagemodelchat/metadata/workflow-run-configuration/local.json
 
b/integration-tests/languagemodelchat/metadata/workflow-run-configuration/local.json
new file mode 100644
index 0000000000..ddb388679a
--- /dev/null
+++ 
b/integration-tests/languagemodelchat/metadata/workflow-run-configuration/local.json
@@ -0,0 +1,9 @@
+{
+  "engineRunConfiguration": {
+    "Local": {
+      "safe_mode": false
+    }
+  },
+  "name": "local",
+  "description": "Runs your workflows locally with the standard local Hop 
workflow engine"
+}
diff --git a/integration-tests/languagemodelchat/project-config.json 
b/integration-tests/languagemodelchat/project-config.json
new file mode 100644
index 0000000000..241753d668
--- /dev/null
+++ b/integration-tests/languagemodelchat/project-config.json
@@ -0,0 +1,15 @@
+{
+  "metadataBaseFolder": "${PROJECT_HOME}/metadata",
+  "unitTestsBasePath": "${PROJECT_HOME}",
+  "dataSetsCsvFolder": "${PROJECT_HOME}/datasets",
+  "enforcingExecutionInHome": true,
+  "config": {
+    "variables": [
+      {
+        "name": "HOP_LICENSE_HEADER_FILE",
+        "value": "${PROJECT_HOME}/../asf-header.txt",
+        "description": "This will automatically serialize the ASF license 
header into pipelines and workflows in the integration test projects"
+      }
+    ]
+  }
+}
diff --git a/plugins/transforms/languagemodelchat/pom.xml 
b/plugins/transforms/languagemodelchat/pom.xml
index d81edd2047..6500b16270 100644
--- a/plugins/transforms/languagemodelchat/pom.xml
+++ b/plugins/transforms/languagemodelchat/pom.xml
@@ -30,10 +30,7 @@
     <name>Hop Plugins Transforms Language Model Chat</name>
 
     <properties>
-        <hop.pluginlib.exclude1>slf4j-tinylog</hop.pluginlib.exclude1>
-        <hop.pluginlib.exclude2>tinylog-impl</hop.pluginlib.exclude2>
-        <hop.pluginlib.exclude3>tinylog-api</hop.pluginlib.exclude3>
-        <langchain4j.version>0.29.1</langchain4j.version>
+        <langchain4j.version>1.20.0</langchain4j.version>
     </properties>
 
     <dependencyManagement>
@@ -64,9 +61,16 @@
             <artifactId>langchain4j-core</artifactId>
             <version>${langchain4j.version}</version>
         </dependency>
+        <!-- The Hugging Face inference API is called through this client 
directly: the
+             langchain4j-hugging-face module is a beta artifact that upstream 
deprecated for removal. -->
         <dependency>
             <groupId>dev.langchain4j</groupId>
-            <artifactId>langchain4j-hugging-face</artifactId>
+            <artifactId>langchain4j-http-client</artifactId>
+            <version>${langchain4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-http-client-jdk</artifactId>
             <version>${langchain4j.version}</version>
         </dependency>
         <dependency>
@@ -84,57 +88,6 @@
             <artifactId>langchain4j-open-ai</artifactId>
             <version>${langchain4j.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.tinylog</groupId>
-            <artifactId>slf4j-tinylog</artifactId>
-            <version>2.6.2</version>
-        </dependency>
-        <dependency>
-            <groupId>org.tinylog</groupId>
-            <artifactId>tinylog-impl</artifactId>
-            <version>2.6.2</version>
-        </dependency>
-        <dependency>
-            <groupId>com.google.code.gson</groupId>
-            <artifactId>gson</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.google.errorprone</groupId>
-            <artifactId>error_prone_annotations</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.squareup.okhttp3</groupId>
-            <artifactId>okhttp</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.squareup.okio</groupId>
-            <artifactId>okio</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.squareup.okio</groupId>
-            <artifactId>okio-jvm</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.jetbrains</groupId>
-            <artifactId>annotations</artifactId>
-            <scope>provided</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.jetbrains.kotlin</groupId>
-            <artifactId>kotlin-stdlib</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.jetbrains.kotlin</groupId>
-            <artifactId>kotlin-stdlib-jdk8</artifactId>
-            <scope>provided</scope>
-        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointFactoryCreator.java
 
b/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointFactoryCreator.java
deleted file mode 100644
index 0db39bd924..0000000000
--- 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointFactoryCreator.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package dev.langchain4j.model.huggingface;
-
-import static dev.langchain4j.spi.ServiceHelper.loadFactories;
-
-import dev.langchain4j.model.huggingface.client.HuggingFaceClient;
-import dev.langchain4j.model.huggingface.spi.HuggingFaceClientFactory;
-
-class DedicatedEndpointFactoryCreator {
-
-  private DedicatedEndpointFactoryCreator() {}
-
-  static final HuggingFaceClientFactory FACTORY = factory();
-
-  private static HuggingFaceClientFactory factory() {
-    for (HuggingFaceClientFactory factory : 
loadFactories(HuggingFaceClientFactory.class)) {
-      return factory;
-    }
-    return new DefaultHuggingFaceClientFactory();
-  }
-
-  static class DefaultHuggingFaceClientFactory implements 
HuggingFaceClientFactory {
-
-    @Override
-    public HuggingFaceClient create(Input input) {
-      return new DedicatedEndpointHuggingFaceClient(
-          input.apiKey(), input.modelId(), input.timeout());
-    }
-  }
-}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceApi.java
 
b/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceApi.java
deleted file mode 100644
index f1f052b0e5..0000000000
--- 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceApi.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package dev.langchain4j.model.huggingface;
-
-import dev.langchain4j.model.huggingface.client.EmbeddingRequest;
-import dev.langchain4j.model.huggingface.client.TextGenerationRequest;
-import dev.langchain4j.model.huggingface.client.TextGenerationResponse;
-import java.util.List;
-import retrofit2.Call;
-import retrofit2.http.Body;
-import retrofit2.http.Headers;
-import retrofit2.http.POST;
-
-interface DedicatedEndpointHuggingFaceApi {
-
-  @POST("/")
-  @Headers({"Content-Type: application/json"})
-  Call<List<TextGenerationResponse>> generate(@Body TextGenerationRequest 
request);
-
-  @POST("/")
-  @Headers({"Content-Type: application/json"})
-  Call<List<float[]>> embed(@Body EmbeddingRequest request);
-}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceChatModel.java
 
b/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceChatModel.java
deleted file mode 100644
index 02362490c9..0000000000
--- 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceChatModel.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package dev.langchain4j.model.huggingface;
-
-import static dev.langchain4j.internal.Utils.isNullOrBlank;
-import static 
dev.langchain4j.model.huggingface.HuggingFaceModelName.TII_UAE_FALCON_7B_INSTRUCT;
-import static java.util.stream.Collectors.joining;
-
-import dev.langchain4j.data.message.AiMessage;
-import dev.langchain4j.data.message.ChatMessage;
-import dev.langchain4j.model.chat.ChatLanguageModel;
-import dev.langchain4j.model.huggingface.client.HuggingFaceClient;
-import dev.langchain4j.model.huggingface.client.Options;
-import dev.langchain4j.model.huggingface.client.Parameters;
-import dev.langchain4j.model.huggingface.client.TextGenerationRequest;
-import dev.langchain4j.model.huggingface.client.TextGenerationResponse;
-import dev.langchain4j.model.huggingface.spi.HuggingFaceClientFactory;
-import dev.langchain4j.model.output.Response;
-import java.time.Duration;
-import java.util.List;
-
-public class DedicatedEndpointHuggingFaceChatModel implements 
ChatLanguageModel {
-
-  private final HuggingFaceClient client;
-  private final Double temperature;
-  private final Integer maxNewTokens;
-  private final Boolean returnFullText;
-  private final Boolean waitForModel;
-
-  public DedicatedEndpointHuggingFaceChatModel(
-      String accessToken,
-      String endpointUrl,
-      Duration timeout,
-      Double temperature,
-      Integer maxNewTokens,
-      Boolean returnFullText,
-      Boolean waitForModel) {
-    this(
-        DedicatedEndpointHuggingFaceChatModel.builder()
-            .accessToken(accessToken)
-            .endpointUrl(endpointUrl)
-            .timeout(timeout)
-            .temperature(temperature)
-            .maxNewTokens(maxNewTokens)
-            .returnFullText(returnFullText)
-            .waitForModel(waitForModel));
-  }
-
-  public DedicatedEndpointHuggingFaceChatModel(Builder builder) {
-    this.client =
-        DedicatedEndpointFactoryCreator.FACTORY.create(
-            new HuggingFaceClientFactory.Input() {
-              @Override
-              public String apiKey() {
-                return builder.accessToken;
-              }
-
-              @Override
-              public String modelId() {
-                return builder.endpointUrl;
-              }
-
-              @Override
-              public Duration timeout() {
-                return builder.timeout;
-              }
-            });
-    this.temperature = builder.temperature;
-    this.maxNewTokens = builder.maxNewTokens;
-    this.returnFullText = builder.returnFullText;
-    this.waitForModel = builder.waitForModel;
-  }
-
-  @Override
-  public Response<AiMessage> generate(List<ChatMessage> messages) {
-
-    TextGenerationRequest request =
-        TextGenerationRequest.builder()
-            
.inputs(messages.stream().map(ChatMessage::text).collect(joining("\n")))
-            .parameters(
-                Parameters.builder()
-                    .temperature(temperature)
-                    .maxNewTokens(maxNewTokens)
-                    .returnFullText(returnFullText)
-                    .build())
-            .options(Options.builder().waitForModel(waitForModel).build())
-            .build();
-
-    TextGenerationResponse textGenerationResponse = client.chat(request);
-
-    return 
Response.from(AiMessage.from(textGenerationResponse.generatedText()));
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  public static final class Builder {
-
-    private String accessToken;
-    private String endpointUrl = TII_UAE_FALCON_7B_INSTRUCT;
-    private Duration timeout = Duration.ofSeconds(15);
-    private Double temperature;
-    private Integer maxNewTokens;
-    private Boolean returnFullText = false;
-    private Boolean waitForModel = true;
-
-    public Builder accessToken(String accessToken) {
-      this.accessToken = accessToken;
-      return this;
-    }
-
-    public Builder endpointUrl(String endpointUrl) {
-      if (endpointUrl != null) {
-        this.endpointUrl = endpointUrl;
-      }
-      return this;
-    }
-
-    public Builder timeout(Duration timeout) {
-      if (timeout != null) {
-        this.timeout = timeout;
-      }
-      return this;
-    }
-
-    public Builder temperature(Double temperature) {
-      this.temperature = temperature;
-      return this;
-    }
-
-    public Builder maxNewTokens(Integer maxNewTokens) {
-      this.maxNewTokens = maxNewTokens;
-      return this;
-    }
-
-    public Builder returnFullText(Boolean returnFullText) {
-      if (returnFullText != null) {
-        this.returnFullText = returnFullText;
-      }
-      return this;
-    }
-
-    public Builder waitForModel(Boolean waitForModel) {
-      if (waitForModel != null) {
-        this.waitForModel = waitForModel;
-      }
-      return this;
-    }
-
-    public DedicatedEndpointHuggingFaceChatModel build() {
-      if (isNullOrBlank(accessToken)) {
-        throw new IllegalArgumentException(
-            "HuggingFace access token must be defined. It can be generated 
here: https://huggingface.co/settings/tokens";);
-      }
-      return new DedicatedEndpointHuggingFaceChatModel(this);
-    }
-  }
-
-  public static DedicatedEndpointHuggingFaceChatModel withAccessToken(String 
accessToken) {
-    return builder().accessToken(accessToken).build();
-  }
-}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceClient.java
 
b/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceClient.java
deleted file mode 100644
index b694c72f52..0000000000
--- 
a/plugins/transforms/languagemodelchat/src/main/java/dev/langchain4j/model/huggingface/DedicatedEndpointHuggingFaceClient.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package dev.langchain4j.model.huggingface;
-
-import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
-import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import dev.langchain4j.model.huggingface.client.EmbeddingRequest;
-import dev.langchain4j.model.huggingface.client.HuggingFaceClient;
-import dev.langchain4j.model.huggingface.client.TextGenerationRequest;
-import dev.langchain4j.model.huggingface.client.TextGenerationResponse;
-import java.io.IOException;
-import java.time.Duration;
-import java.util.List;
-import okhttp3.OkHttpClient;
-import org.apache.hop.core.exception.HopRuntimeException;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.converter.gson.GsonConverterFactory;
-
-class DedicatedEndpointHuggingFaceClient implements HuggingFaceClient {
-
-  private final DedicatedEndpointHuggingFaceApi huggingFaceApi;
-  private final String endpointUrl;
-
-  DedicatedEndpointHuggingFaceClient(String apiKey, String endpointUrl, 
Duration timeout) {
-
-    this.endpointUrl = ensureNotBlank(endpointUrl, "endpointUrl");
-    OkHttpClient okHttpClient =
-        new OkHttpClient.Builder()
-            .addInterceptor(new ApiKeyInsertingInterceptor(apiKey))
-            .callTimeout(timeout)
-            .connectTimeout(timeout)
-            .readTimeout(timeout)
-            .writeTimeout(timeout)
-            .build();
-
-    Gson gson = new 
GsonBuilder().setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
-
-    Retrofit retrofit =
-        new Retrofit.Builder()
-            .baseUrl(endpointUrl)
-            .client(okHttpClient)
-            .addConverterFactory(GsonConverterFactory.create(gson))
-            .build();
-
-    this.huggingFaceApi = 
retrofit.create(DedicatedEndpointHuggingFaceApi.class);
-  }
-
-  @Override
-  public TextGenerationResponse chat(TextGenerationRequest request) {
-    return generate(request);
-  }
-
-  @Override
-  public TextGenerationResponse generate(TextGenerationRequest request) {
-    try {
-      retrofit2.Response<List<TextGenerationResponse>> retrofitResponse =
-          huggingFaceApi.generate(request).execute();
-
-      if (retrofitResponse.isSuccessful()) {
-        return toOneResponse(retrofitResponse);
-      } else {
-        throw toException(retrofitResponse);
-      }
-    } catch (IOException e) {
-      throw new HopRuntimeException(e);
-    }
-  }
-
-  private static TextGenerationResponse toOneResponse(
-      Response<List<TextGenerationResponse>> retrofitResponse) {
-    List<TextGenerationResponse> responses = retrofitResponse.body();
-    if (responses != null && responses.size() == 1) {
-      return responses.get(0);
-    } else {
-      throw new HopRuntimeException(
-          "Expected only one generated_text, but was: "
-              + (responses == null ? 0 : responses.size()));
-    }
-  }
-
-  @Override
-  public List<float[]> embed(EmbeddingRequest request) {
-    try {
-      retrofit2.Response<List<float[]>> retrofitResponse = 
huggingFaceApi.embed(request).execute();
-      if (retrofitResponse.isSuccessful()) {
-        return retrofitResponse.body();
-      } else {
-        throw toException(retrofitResponse);
-      }
-    } catch (IOException e) {
-      throw new HopRuntimeException(e);
-    }
-  }
-
-  private static HopRuntimeException 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 HopRuntimeException(errorMessage);
-  }
-}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChat.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChat.java
index d30b38f6ad..65a6d5a1c4 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChat.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChat.java
@@ -30,9 +30,8 @@ import static org.apache.commons.lang3.StringUtils.trim;
 import static org.apache.hop.core.util.Utils.isEmpty;
 import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.ui.i18nUtil.i18n;
 
-import dev.langchain4j.data.message.AiMessage;
 import dev.langchain4j.data.message.ChatMessage;
-import dev.langchain4j.model.output.Response;
+import dev.langchain4j.model.chat.response.ChatResponse;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
@@ -255,7 +254,7 @@ public class LanguageModelChat extends 
BaseTransform<LanguageModelChatMeta, Lang
     } else {
       Instant inferenceStart = now();
       try {
-        Response<AiMessage> ai = facade.generate(messageList);
+        ChatResponse ai = facade.chat(messageList);
         inferenceTime = between(inferenceStart, now()).toMillis();
         inputTokenCount =
             ai.tokenUsage() == null || ai.tokenUsage().inputTokenCount() == 
null
@@ -271,9 +270,9 @@ public class LanguageModelChat extends 
BaseTransform<LanguageModelChatMeta, Lang
                 : ai.tokenUsage().totalTokenCount().longValue();
         finishReason = ai.finishReason() == null ? null : 
ai.finishReason().name();
         if (meta.isOutputChatJson()) {
-          output = facade.messagesToOutput(messageList, ai.content().text());
+          output = facade.messagesToOutput(messageList, ai.aiMessage().text());
         } else {
-          output = ai.content().text();
+          output = ai.aiMessage().text();
         }
       } catch (Exception e) {
         inferenceTime = between(inferenceStart, now()).toMillis();
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMeta.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMeta.java
index 553049ed11..01e9723eb4 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMeta.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMeta.java
@@ -21,13 +21,13 @@ import static 
org.apache.hop.core.ICheckResult.TYPE_RESULT_ERROR;
 import static org.apache.hop.core.ICheckResult.TYPE_RESULT_OK;
 import static org.apache.hop.core.util.Utils.isEmpty;
 import static org.apache.hop.i18n.BaseMessages.getString;
-import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.ANTHROPIC_CLAUDE_3_HAIKU_20240307;
-import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.ANTHROPIC_CLAUDE_3_OPUS_20240229;
-import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.HUGGING_FACE_LLAMA3_70B_INSTRUCT;
+import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.ANTHROPIC_CLAUDE_HAIKU_4_5;
+import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.ANTHROPIC_CLAUDE_SONNET_5;
+import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.HUGGING_FACE_LLAMA3_3_70B_INSTRUCT;
 import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.MISTRAL_LARGE_LATEST;
-import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.OLLAMA_PHI3_3_8B;
-import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.OPENAI_GPT_4O;
-import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.OPENAI_GPT_4O_MINI;
+import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.OLLAMA_QWEN3;
+import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.OPENAI_GPT_5_6_LUNA;
+import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.LanguageModelChatModelName.OPENAI_GPT_6_ASTRA;
 import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.ModelType.OPEN_AI;
 
 import java.util.List;
@@ -74,9 +74,12 @@ public class LanguageModelChatMeta
 
   // OpenAI
   @HopMetadataProperty private String openAiBaseUrl = 
"https://api.openai.com/v1";;
-  @HopMetadataProperty private String openAiApiKey = "OPENAI_API_KEY}";
+
+  @HopMetadataProperty(password = true)
+  private String openAiApiKey = "OPENAI_API_KEY";
+
   @HopMetadataProperty private String openAiOrganizationId;
-  @HopMetadataProperty private String openAiModelName = 
OPENAI_GPT_4O.toString();
+  @HopMetadataProperty private String openAiModelName = 
OPENAI_GPT_6_ASTRA.toString();
   @HopMetadataProperty private Double openAiTemperature = 0.7;
   @HopMetadataProperty private Double openAiTopP;
   // TODO private List<String> openAiStop;
@@ -94,13 +97,15 @@ public class LanguageModelChatMeta
   @HopMetadataProperty private Integer openAiProxyPort = 30000;
   @HopMetadataProperty private boolean openAiLogRequests = false;
   @HopMetadataProperty private boolean openAiLogResponses = false;
+
   // TODO private Tokenizer openAiTokenizer = new OpenAiTokenizer();
 
   // HuggingFace
-  @HopMetadataProperty private String huggingFaceAccessToken = 
"HF_ACCESS_TOKEN";
+  @HopMetadataProperty(password = true)
+  private String huggingFaceAccessToken = "HF_ACCESS_TOKEN";
 
   @HopMetadataProperty
-  private String huggingFaceModelId = 
HUGGING_FACE_LLAMA3_70B_INSTRUCT.toString();
+  private String huggingFaceModelId = 
HUGGING_FACE_LLAMA3_3_70B_INSTRUCT.toString();
 
   @HopMetadataProperty private Integer huggingFaceTimeout = 15;
   @HopMetadataProperty private Double huggingFaceTemperature;
@@ -110,7 +115,10 @@ public class LanguageModelChatMeta
 
   // Mistral
   @HopMetadataProperty private String mistralBaseUrl = 
"https://api.mistral.ai/v1";;
-  @HopMetadataProperty private String mistralApiKey = "MISTRAL_API_KEY";
+
+  @HopMetadataProperty(password = true)
+  private String mistralApiKey = "MISTRAL_API_KEY";
+
   @HopMetadataProperty private String mistralModelName = 
MISTRAL_LARGE_LATEST.toString();
   @HopMetadataProperty private Double mistralTemperature;
   @HopMetadataProperty private Double mistralTopP;
@@ -125,7 +133,7 @@ public class LanguageModelChatMeta
 
   // Ollama
   @HopMetadataProperty private String ollamaImageEndpoint;
-  @HopMetadataProperty private String ollamaModelName = 
OLLAMA_PHI3_3_8B.toString();
+  @HopMetadataProperty private String ollamaModelName = 
OLLAMA_QWEN3.toString();
   @HopMetadataProperty private Double ollamaTemperature;
   @HopMetadataProperty private Integer ollamaTopK;
   @HopMetadataProperty private Double ollamaTopP;
@@ -140,11 +148,13 @@ public class LanguageModelChatMeta
 
   // Anthropic
   @HopMetadataProperty private String anthropicBaseUrl = 
"https://api.anthropic.com/v1/";;
-  @HopMetadataProperty private String anthropicApiKey = "ANTHROPIC_API_KEY";
+
+  @HopMetadataProperty(password = true)
+  private String anthropicApiKey = "ANTHROPIC_API_KEY";
+
   @HopMetadataProperty private String anthropicVersion = "2023-06-01";
 
-  @HopMetadataProperty
-  private String anthropicModelName = 
ANTHROPIC_CLAUDE_3_OPUS_20240229.toString();
+  @HopMetadataProperty private String anthropicModelName = 
ANTHROPIC_CLAUDE_SONNET_5.toString();
 
   @HopMetadataProperty private Double anthropicTemperature;
   @HopMetadataProperty private Double anthropicTopP;
@@ -174,7 +184,7 @@ public class LanguageModelChatMeta
 
     openAiBaseUrl = "https://api.openai.com/v1";;
     openAiApiKey = "OPENAI_API_KEY";
-    openAiModelName = OPENAI_GPT_4O_MINI.toString();
+    openAiModelName = OPENAI_GPT_5_6_LUNA.toString();
     openAiResponseFormat = "text";
     openAiTemperature = 0.7;
     openAiTimeout = 60;
@@ -186,7 +196,7 @@ public class LanguageModelChatMeta
     openAiLogResponses = false;
 
     huggingFaceAccessToken = "HF_ACCESS_TOKEN";
-    huggingFaceModelId = HUGGING_FACE_LLAMA3_70B_INSTRUCT.toString();
+    huggingFaceModelId = HUGGING_FACE_LLAMA3_3_70B_INSTRUCT.toString();
     huggingFaceTimeout = 15;
     huggingFaceReturnFullText = false;
     huggingFaceWaitForModel = true;
@@ -201,14 +211,14 @@ public class LanguageModelChatMeta
     mistralMaxRetries = 3;
 
     ollamaImageEndpoint = "";
-    ollamaModelName = OLLAMA_PHI3_3_8B.toString();
+    ollamaModelName = OLLAMA_QWEN3.toString();
     ollamaTimeout = 60;
     ollamaMaxRetries = 3;
 
     anthropicBaseUrl = "https://api.anthropic.com/v1/";;
     anthropicApiKey = "ANTHROPIC_API_KEY";
     anthropicVersion = "2023-06-01";
-    anthropicModelName = ANTHROPIC_CLAUDE_3_HAIKU_20240307.toString();
+    anthropicModelName = ANTHROPIC_CLAUDE_HAIKU_4_5.toString();
     anthropicMaxTokens = 1024;
     anthropicTimeout = 15;
     anthropicMaxRetries = 3;
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ChatMessages.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ChatMessages.java
new file mode 100644
index 0000000000..ec1df7fd5a
--- /dev/null
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ChatMessages.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.languagemodelchat.internals;
+
+import static java.util.stream.Collectors.joining;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.TextContent;
+import dev.langchain4j.data.message.UserMessage;
+
+/**
+ * Reads the text out of a chat message. ChatMessage itself no longer exposes 
text(): a user message
+ * can carry several contents (text and images), so the text parts are 
collected here.
+ */
+public class ChatMessages {
+
+  private ChatMessages() {}
+
+  public static String text(ChatMessage message) {
+    if (message == null) {
+      return null;
+    }
+    return switch (message.type()) {
+      case SYSTEM -> ((SystemMessage) message).text();
+      case AI -> ((AiMessage) message).text();
+      case USER -> userText((UserMessage) message);
+      default -> null;
+    };
+  }
+
+  private static String userText(UserMessage message) {
+    if (message.hasSingleText()) {
+      return message.singleText();
+    }
+    return message.contents().stream()
+        .filter(TextContent.class::isInstance)
+        .map(content -> ((TextContent) content).text())
+        .collect(joining("\n"));
+  }
+}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelChatModelName.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelChatModelName.java
index aedfb9b942..a9535b793c 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelChatModelName.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelChatModelName.java
@@ -19,34 +19,35 @@ package 
org.apache.hop.pipeline.transforms.languagemodelchat.internals;
 
 public enum LanguageModelChatModelName {
 
-  // https://platform.openai.com/docs/models/continuous-model-upgrades
-  OPENAI_GPT_4O("gpt-4o"),
-  OPENAI_GPT_4O_MINI("gpt-4o-mini"),
-  OPENAI_GPT_O1_MINI("o1-mini"),
-  OPENAI_GPT_O1_PREVIEW("o1-preview"),
-  OPENAI_GPT_4_TURBO("gpt-4-turbo"),
+  // https://platform.openai.com/docs/models
+  OPENAI_GPT_6_ASTRA("gpt-6-astra"),
+  OPENAI_GPT_5_6_TERRA("gpt-5.6-terra"),
+  OPENAI_GPT_5_6_LUNA("gpt-5.6-luna"),
+  OPENAI_GPT_5_6_SOL("gpt-5.6-sol"),
 
   // https://docs.mistral.ai/getting-started/models/
-  OPEN_MISTRAL_7B("open-mistral-7b"),
-  OPEN_MIXTRAL_8X7B("open-mixtral-8x7b"),
-  MISTRAL_SMALL_LATEST("mistral-small-latest"),
-  MISTRAL_MEDIUM_LATEST("mistral-medium-latest"),
   MISTRAL_LARGE_LATEST("mistral-large-latest"),
+  MISTRAL_MEDIUM_LATEST("mistral-medium-latest"),
+  MISTRAL_SMALL_LATEST("mistral-small-latest"),
+  MAGISTRAL_MEDIUM_2509("magistral-medium-2509"),
 
   // https://ollama.com/library
-  OLLAMA_LLAMA3_8B("llama3"),
-  OLLAMA_LLAMA3_70B("llama3:70b"),
-  OLLAMA_PHI3_3_8B("phi3"),
-  OLLAMA_PHI3_14B("phi3:medium"),
-
-  // https://ui.endpoints.huggingface.co/catalog
-  HUGGING_FACE_LLAMA3_70B_INSTRUCT("meta-llama/Meta-Llama-3-70B-Instruct"),
+  OLLAMA_LLAMA3_3("llama3.3"),
+  OLLAMA_QWEN3("qwen3"),
+  OLLAMA_PHI4("phi4"),
+  OLLAMA_GEMMA3("gemma3"),
+  OLLAMA_DEEPSEEK_R1("deepseek-r1"),
+
+  // https://huggingface.co/models
+  HUGGING_FACE_QWEN3_8B("Qwen/Qwen3-8B"),
+  HUGGING_FACE_LLAMA3_3_70B_INSTRUCT("meta-llama/Llama-3.3-70B-Instruct"),
   HUGGING_FACE_MISTRAL_7B_INSTRUCT("mistralai/Mistral-7B-Instruct-v0.3"),
+  HUGGING_FACE_GPT_OSS_20B("openai/gpt-oss-20b"),
 
-  // https://docs.anthropic.com/en/docs/models-overview
-  ANTHROPIC_CLAUDE_3_OPUS_20240229("claude-3-opus-20240229"),
-  ANTHROPIC_CLAUDE_3_SONNET_20240229("claude-3-sonnet-20240229"),
-  ANTHROPIC_CLAUDE_3_HAIKU_20240307("claude-3-haiku-20240307");
+  // https://docs.claude.com/en/docs/about-claude/models/overview
+  ANTHROPIC_CLAUDE_OPUS_5("claude-opus-5"),
+  ANTHROPIC_CLAUDE_SONNET_5("claude-sonnet-5"),
+  ANTHROPIC_CLAUDE_HAIKU_4_5("claude-haiku-4-5");
 
   private final String stringValue;
 
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacade.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacade.java
index 202ba1a1d0..03aa5a4b23 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacade.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacade.java
@@ -21,10 +21,9 @@ import static com.squareup.moshi.Types.newParameterizedType;
 import static dev.langchain4j.data.message.UserMessage.userMessage;
 import static java.lang.String.format;
 import static java.lang.System.getenv;
-import static java.net.Proxy.Type.HTTP;
 import static java.time.Duration.ofSeconds;
+import static org.apache.commons.lang3.StringUtils.containsIgnoreCase;
 import static org.apache.commons.lang3.StringUtils.isBlank;
-import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase;
 import static org.apache.commons.lang3.StringUtils.trimToNull;
 import static org.apache.commons.lang3.Validate.isTrue;
 import static org.apache.commons.lang3.Validate.notNull;
@@ -32,23 +31,24 @@ import static 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.Mes
 
 import com.squareup.moshi.JsonAdapter;
 import com.squareup.moshi.Moshi;
-import dev.langchain4j.data.message.AiMessage;
 import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.http.client.jdk.JdkHttpClientBuilder;
 import dev.langchain4j.model.anthropic.AnthropicChatModel;
-import dev.langchain4j.model.chat.ChatLanguageModel;
-import dev.langchain4j.model.huggingface.DedicatedEndpointHuggingFaceChatModel;
-import dev.langchain4j.model.huggingface.HuggingFaceChatModel;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ResponseFormat;
+import dev.langchain4j.model.chat.response.ChatResponse;
 import dev.langchain4j.model.mistralai.MistralAiChatModel;
 import dev.langchain4j.model.ollama.OllamaChatModel;
 import dev.langchain4j.model.openai.OpenAiChatModel;
-import dev.langchain4j.model.output.Response;
 import java.net.InetSocketAddress;
-import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.http.HttpClient;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.variables.IVariables;
 import 
org.apache.hop.pipeline.transforms.languagemodelchat.LanguageModelChatMeta;
+import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.huggingface.HuggingFaceChatModel;
 
 public class LanguageModelFacade {
 
@@ -57,7 +57,7 @@ public class LanguageModelFacade {
   private final JsonAdapter<List<BaseMessage>> outputJsonAdapter;
   private final LanguageModel lm;
   private final IVariables variables;
-  private ChatLanguageModel model;
+  private ChatModel model;
 
   public LanguageModelFacade(IVariables variables, LanguageModelChatMeta meta) 
{
     this.variables = variables;
@@ -93,7 +93,7 @@ public class LanguageModelFacade {
     return variables.resolve(trimToNull(param));
   }
 
-  private ChatLanguageModel createOpenAiModel() {
+  private ChatModel createOpenAiModel() {
 
     String baseUrl = resolve(meta.getOpenAiBaseUrl());
     String apiKey = trimToNull(resolve(meta.getOpenAiApiKey()));
@@ -124,7 +124,7 @@ public class LanguageModelFacade {
             .maxTokens(maxTokens)
             .presencePenalty(presencePenalty)
             .frequencyPenalty(frequencyPenalty)
-            .responseFormat(responseFormat)
+            .responseFormat(toResponseFormat(responseFormat))
             .seed(seed)
             .user(user)
             .timeout(timeout == null ? null : ofSeconds(timeout))
@@ -133,19 +133,21 @@ public class LanguageModelFacade {
             .logResponses(logResponses);
 
     if (meta.isOpenAiUseProxy()) {
-      builder.proxy(
-          new Proxy(
-              HTTP, new InetSocketAddress(meta.getOpenAiProxyHost(), 
meta.getOpenAiProxyPort())));
+      builder.httpClientBuilder(
+          new JdkHttpClientBuilder()
+              .httpClientBuilder(
+                  HttpClient.newBuilder()
+                      .proxy(
+                          ProxySelector.of(
+                              new InetSocketAddress(
+                                  meta.getOpenAiProxyHost(), 
meta.getOpenAiProxyPort())))));
     }
 
     return builder.build();
   }
 
-  private ChatLanguageModel createHuggingFaceModel() {
+  private ChatModel createHuggingFaceModel() {
     String modelResource = resolve(meta.getHuggingFaceModelId());
-    boolean dedicated =
-        startsWithIgnoreCase(modelResource, "http://";)
-            || startsWithIgnoreCase(modelResource, "https://";);
 
     String accessToken = trimToNull(resolve(meta.getHuggingFaceAccessToken()));
     accessToken =
@@ -157,30 +159,18 @@ public class LanguageModelFacade {
     boolean returnFullText = meta.isHuggingFaceReturnFullText();
     boolean waitForModel = meta.isHuggingFaceWaitForModel();
 
-    if (dedicated) {
-      return DedicatedEndpointHuggingFaceChatModel.builder()
-          .accessToken(accessToken)
-          .endpointUrl(modelResource)
-          .timeout(timeout == null ? null : ofSeconds(timeout))
-          .temperature(temperature)
-          .maxNewTokens(maxNewTokens)
-          .returnFullText(returnFullText)
-          .waitForModel(waitForModel)
-          .build();
-    } else {
-      return HuggingFaceChatModel.builder()
-          .accessToken(accessToken)
-          .modelId(modelResource)
-          .timeout(timeout == null ? null : ofSeconds(timeout))
-          .temperature(temperature)
-          .maxNewTokens(maxNewTokens)
-          .returnFullText(returnFullText)
-          .waitForModel(waitForModel)
-          .build();
-    }
+    return HuggingFaceChatModel.builder()
+        .accessToken(accessToken)
+        .modelResource(modelResource)
+        .timeout(timeout == null ? null : ofSeconds(timeout))
+        .temperature(temperature)
+        .maxNewTokens(maxNewTokens)
+        .returnFullText(returnFullText)
+        .waitForModel(waitForModel)
+        .build();
   }
 
-  private ChatLanguageModel createOllamaModel() {
+  private ChatModel createOllamaModel() {
     String modelName = resolve(meta.getOllamaModelName());
 
     if (isBlank(modelName)) {
@@ -218,13 +208,13 @@ public class LanguageModelFacade {
         .seed(seed)
         .numPredict(numPredict)
         .numCtx(numCtx)
-        .format(format)
+        .responseFormat(toResponseFormat(format))
         .timeout(timeout == null ? null : ofSeconds(timeout))
         .maxRetries(maxRetries)
         .build();
   }
 
-  private ChatLanguageModel createAnthropicModel() {
+  private ChatModel createAnthropicModel() {
 
     String baseUrl = resolve(meta.getAnthropicBaseUrl());
     String apiKey = trimToNull(resolve(meta.getAnthropicApiKey()));
@@ -257,7 +247,7 @@ public class LanguageModelFacade {
         .build();
   }
 
-  private ChatLanguageModel createMistralModel() {
+  private ChatModel createMistralModel() {
     String baseUrl = resolve(meta.getMistralBaseUrl());
     String apiKey = trimToNull(resolve(meta.getMistralApiKey()));
     apiKey = isBlank(apiKey) ? trimToNull(getenv(meta.getMistralApiKey())) : 
apiKey;
@@ -283,7 +273,7 @@ public class LanguageModelFacade {
         .maxTokens(maxTokens)
         .safePrompt(safePrompt)
         .randomSeed(randomSeed)
-        .responseFormat(responseFormat)
+        .responseFormat(toResponseFormat(responseFormat))
         .timeout(timeout == null ? null : ofSeconds(timeout))
         .logRequests(logRequests)
         .logResponses(logResponses)
@@ -291,6 +281,19 @@ public class LanguageModelFacade {
         .build();
   }
 
+  /**
+   * The providers take a response format object instead of the free format 
string the dialogs ask
+   * for. Anything mentioning json asks for json, anything else for text. Open 
AI does read a string
+   * as well, but only recognises "json_object" there and drops every other 
value.
+   */
+  private static ResponseFormat toResponseFormat(String format) {
+    String value = trimToNull(format);
+    if (value == null) {
+      return null;
+    }
+    return containsIgnoreCase(value, "json") ? ResponseFormat.JSON : 
ResponseFormat.TEXT;
+  }
+
   @SuppressWarnings("java:S131")
   public String messagesToOutput(List<ChatMessage> chat, String assistant) {
     // TODO fetch these from the meta, hard code for now
@@ -300,7 +303,7 @@ public class LanguageModelFacade {
 
     List<BaseMessage> messages = new ArrayList<>();
     for (ChatMessage c : chat) {
-      String text = c.text();
+      String text = ChatMessages.text(c);
       switch (c.type()) {
         case SYSTEM -> messages.add(new BaseMessage(systemRoleName, text));
         case USER -> messages.add(new BaseMessage(userRoleName, text));
@@ -337,11 +340,11 @@ public class LanguageModelFacade {
     return messageList;
   }
 
-  public Response<AiMessage> generate(List<ChatMessage> messages) {
-    return model().generate(messages);
+  public ChatResponse chat(List<ChatMessage> messages) {
+    return model().chat(messages);
   }
 
-  public ChatLanguageModel model() {
+  public ChatModel model() {
     if (model == null) {
       createModel();
     }
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/huggingface/HuggingFaceChatModel.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/huggingface/HuggingFaceChatModel.java
new file mode 100644
index 0000000000..737740131d
--- /dev/null
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/huggingface/HuggingFaceChatModel.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.huggingface;
+
+import static java.util.stream.Collectors.joining;
+import static org.apache.commons.lang3.StringUtils.isBlank;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import java.time.Duration;
+import java.util.Objects;
+import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.ChatMessages;
+import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.huggingface.HuggingFaceClient.Options;
+import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.huggingface.HuggingFaceClient.Parameters;
+import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.huggingface.HuggingFaceClient.TextGenerationRequest;
+
+/**
+ * A chat model on top of the Hugging Face text generation API. The 
langchain4j Hugging Face module
+ * is a beta artifact that upstream deprecated for removal, so the API is 
called here directly.
+ */
+public class HuggingFaceChatModel implements ChatModel {
+
+  private final HuggingFaceClient client;
+  private final Double temperature;
+  private final Integer maxNewTokens;
+  private final Boolean returnFullText;
+  private final Boolean waitForModel;
+
+  public HuggingFaceChatModel(Builder builder) {
+    if (isBlank(builder.accessToken)) {
+      throw new IllegalArgumentException(
+          "HuggingFace access token must be defined. It can be generated here: 
https://huggingface.co/settings/tokens";);
+    }
+    if (isBlank(builder.modelResource)) {
+      throw new IllegalArgumentException(
+          "HuggingFace model id or dedicated endpoint URL must be defined.");
+    }
+    this.client =
+        new HuggingFaceClient(builder.modelResource, builder.accessToken, 
builder.timeout);
+    this.temperature = builder.temperature;
+    this.maxNewTokens = builder.maxNewTokens;
+    this.returnFullText = builder.returnFullText;
+    this.waitForModel = builder.waitForModel;
+  }
+
+  @Override
+  public ChatResponse doChat(ChatRequest chatRequest) {
+    String inputs =
+        chatRequest.messages().stream()
+            .map(ChatMessages::text)
+            .filter(Objects::nonNull)
+            .collect(joining("\n"));
+
+    TextGenerationRequest request =
+        new TextGenerationRequest(
+            inputs,
+            new Parameters(temperature, maxNewTokens, returnFullText),
+            new Options(waitForModel));
+
+    return 
ChatResponse.builder().aiMessage(AiMessage.from(client.generate(request))).build();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static final class Builder {
+    private String accessToken;
+    private String modelResource;
+    private Duration timeout = Duration.ofSeconds(15);
+    private Double temperature;
+    private Integer maxNewTokens;
+    private Boolean returnFullText = false;
+    private Boolean waitForModel = true;
+
+    public Builder accessToken(String accessToken) {
+      this.accessToken = accessToken;
+      return this;
+    }
+
+    /** A Hugging Face model id, or the URL of a dedicated inference endpoint. 
*/
+    public Builder modelResource(String modelResource) {
+      this.modelResource = modelResource;
+      return this;
+    }
+
+    public Builder timeout(Duration timeout) {
+      if (timeout != null) {
+        this.timeout = timeout;
+      }
+      return this;
+    }
+
+    public Builder temperature(Double temperature) {
+      this.temperature = temperature;
+      return this;
+    }
+
+    public Builder maxNewTokens(Integer maxNewTokens) {
+      this.maxNewTokens = maxNewTokens;
+      return this;
+    }
+
+    public Builder returnFullText(Boolean returnFullText) {
+      if (returnFullText != null) {
+        this.returnFullText = returnFullText;
+      }
+      return this;
+    }
+
+    public Builder waitForModel(Boolean waitForModel) {
+      if (waitForModel != null) {
+        this.waitForModel = waitForModel;
+      }
+      return this;
+    }
+
+    public HuggingFaceChatModel build() {
+      return new HuggingFaceChatModel(this);
+    }
+  }
+}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/huggingface/HuggingFaceClient.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/huggingface/HuggingFaceClient.java
new file mode 100644
index 0000000000..29ef4301b4
--- /dev/null
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/huggingface/HuggingFaceClient.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.huggingface;
+
+import static com.squareup.moshi.Types.newParameterizedType;
+import static dev.langchain4j.http.client.HttpMethod.POST;
+import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase;
+
+import com.squareup.moshi.Json;
+import com.squareup.moshi.JsonAdapter;
+import com.squareup.moshi.Moshi;
+import dev.langchain4j.exception.HttpException;
+import dev.langchain4j.http.client.HttpClient;
+import dev.langchain4j.http.client.HttpClientBuilderLoader;
+import dev.langchain4j.http.client.HttpRequest;
+import dev.langchain4j.http.client.SuccessfulHttpResponse;
+import java.time.Duration;
+import java.util.List;
+import org.apache.hop.core.exception.HopRuntimeException;
+
+/**
+ * Calls the Hugging Face text generation API. A model id is sent to the 
Hugging Face inference
+ * router, an http(s) model resource is taken to be a dedicated inference 
endpoint and is called as
+ * it stands.
+ */
+public class HuggingFaceClient {
+
+  private static final String ROUTER_URL = 
"https://router.huggingface.co/hf-inference/models/";;
+
+  private final HttpClient httpClient;
+  private final JsonAdapter<TextGenerationRequest> requestAdapter;
+  private final JsonAdapter<List<TextGenerationResponse>> responseAdapter;
+  private final String url;
+  private final String accessToken;
+
+  public HuggingFaceClient(String modelResource, String accessToken, Duration 
timeout) {
+    this.httpClient =
+        HttpClientBuilderLoader.loadHttpClientBuilder()
+            .connectTimeout(timeout)
+            .readTimeout(timeout)
+            .build();
+    this.url = isDedicatedEndpoint(modelResource) ? modelResource : ROUTER_URL 
+ modelResource;
+    this.accessToken = accessToken;
+
+    Moshi moshi = new Moshi.Builder().build();
+    this.requestAdapter = moshi.adapter(TextGenerationRequest.class);
+    this.responseAdapter =
+        moshi.adapter(newParameterizedType(List.class, 
TextGenerationResponse.class));
+  }
+
+  public static boolean isDedicatedEndpoint(String modelResource) {
+    return startsWithIgnoreCase(modelResource, "http://";)
+        || startsWithIgnoreCase(modelResource, "https://";);
+  }
+
+  public String generate(TextGenerationRequest request) {
+    HttpRequest httpRequest =
+        HttpRequest.builder()
+            .method(POST)
+            .url(url)
+            .addHeader("Content-Type", "application/json")
+            .addHeader("Authorization", "Bearer " + accessToken)
+            .body(requestAdapter.toJson(request))
+            .build();
+
+    SuccessfulHttpResponse httpResponse;
+    try {
+      httpResponse = httpClient.execute(httpRequest);
+    } catch (HttpException e) {
+      throw new HopRuntimeException(
+          "status code: " + e.statusCode() + "; body: " + e.getMessage(), e);
+    }
+
+    List<TextGenerationResponse> responses;
+    try {
+      responses = responseAdapter.fromJson(httpResponse.body());
+    } catch (Exception e) {
+      throw new HopRuntimeException("Could not read the Hugging Face 
response", e);
+    }
+
+    if (responses == null || responses.size() != 1) {
+      throw new HopRuntimeException(
+          "Expected only one generated_text, but was: "
+              + (responses == null ? 0 : responses.size()));
+    }
+
+    return responses.get(0).generatedText;
+  }
+
+  public static class TextGenerationRequest {
+    private String inputs;
+    private Parameters parameters;
+    private Options options;
+
+    public TextGenerationRequest(String inputs, Parameters parameters, Options 
options) {
+      this.inputs = inputs;
+      this.parameters = parameters;
+      this.options = options;
+    }
+  }
+
+  public static class Parameters {
+    private Double temperature;
+
+    @Json(name = "max_new_tokens")
+    private Integer maxNewTokens;
+
+    @Json(name = "return_full_text")
+    private Boolean returnFullText;
+
+    public Parameters(Double temperature, Integer maxNewTokens, Boolean 
returnFullText) {
+      this.temperature = temperature;
+      this.maxNewTokens = maxNewTokens;
+      this.returnFullText = returnFullText;
+    }
+  }
+
+  public static class Options {
+    @Json(name = "wait_for_model")
+    private Boolean waitForModel;
+
+    public Options(Boolean waitForModel) {
+      this.waitForModel = waitForModel;
+    }
+  }
+
+  public static class TextGenerationResponse {
+    @Json(name = "generated_text")
+    private String generatedText;
+  }
+}
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AbstractModelComposite.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AbstractModelComposite.java
index 29f77179d6..e266747943 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AbstractModelComposite.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AbstractModelComposite.java
@@ -39,6 +39,7 @@ import 
org.apache.hop.pipeline.transforms.languagemodelchat.LanguageModelChatMet
 import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.ModelType;
 import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.ui.CompositeParameters;
 import 
org.apache.hop.pipeline.transforms.languagemodelchat.internals.ui.FormDataBuilder;
+import org.apache.hop.ui.core.widget.PasswordTextVar;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.eclipse.swt.custom.CCombo;
 import org.eclipse.swt.layout.FormAttachment;
@@ -87,6 +88,11 @@ public abstract class AbstractModelComposite implements 
IModelComposite {
     return new TextVar(parameters.variables(), composite, SINGLE | LEFT | 
BORDER);
   }
 
+  /** Api keys and access tokens are masked, the way every other secret in Hop 
is. */
+  protected TextVar createPasswordTextVar() {
+    return new PasswordTextVar(parameters.variables(), composite, SINGLE | 
LEFT | BORDER);
+  }
+
   protected Button createButton() {
     return new Button(composite, CHECK);
   }
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AnthropicComposite.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AnthropicComposite.java
index 7b3b7e9741..14291be740 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AnthropicComposite.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/AnthropicComposite.java
@@ -65,7 +65,7 @@ public class AnthropicComposite extends 
AbstractModelComposite {
 
     // API Key
     apiKeyLabel = createLabel();
-    apiKeyInput = createTextVar();
+    apiKeyInput = createPasswordTextVar();
     prepare("ApiKey", apiKeyLabel, apiKeyInput);
 
     // Version
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/HuggingFaceComposite.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/HuggingFaceComposite.java
index 8da752e080..67cd782749 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/HuggingFaceComposite.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/HuggingFaceComposite.java
@@ -47,7 +47,7 @@ public class HuggingFaceComposite extends 
AbstractModelComposite {
 
     // Access Token
     accessTokenLabel = createLabel();
-    accessTokenInput = createTextVar();
+    accessTokenInput = createPasswordTextVar();
     prepare("AccessToken", accessTokenLabel, accessTokenInput);
 
     // Temperature
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/MistralComposite.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/MistralComposite.java
index 2036f750da..a941630318 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/MistralComposite.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/MistralComposite.java
@@ -62,7 +62,7 @@ public class MistralComposite extends AbstractModelComposite {
 
     // API Key
     apiKeyLabel = createLabel();
-    apiKeyInput = createTextVar();
+    apiKeyInput = createPasswordTextVar();
     prepare("ApiKey", apiKeyLabel, apiKeyInput);
 
     // Model Name
diff --git 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/OpenAiComposite.java
 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/OpenAiComposite.java
index eb4c954ee0..7dae1c6d69 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/OpenAiComposite.java
+++ 
b/plugins/transforms/languagemodelchat/src/main/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/ui/models/OpenAiComposite.java
@@ -76,7 +76,7 @@ public class OpenAiComposite extends AbstractModelComposite {
 
     // API Key
     apiKeyLabel = createLabel();
-    apiKeyInput = createTextVar();
+    apiKeyInput = createPasswordTextVar();
     prepare("ApiKey", apiKeyLabel, apiKeyInput);
 
     // Model Name
diff --git 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/fact-checking/bespoke-minicheck.hpl
 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/fact-checking/bespoke-minicheck.hpl
index 4a121e9d31..dd01e56004 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/fact-checking/bespoke-minicheck.hpl
+++ 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/fact-checking/bespoke-minicheck.hpl
@@ -429,7 +429,7 @@ Claim: ${claim}</input_text>
     <openAiLogRequests>false</openAiLogRequests>
     <openAiLogResponses>false</openAiLogResponses>
     <huggingFaceAccessToken>${HF_TOKEN}</huggingFaceAccessToken>
-    <huggingFaceModelId>meta-llama/Llama-3.1-70B-Instruct</huggingFaceModelId>
+    <huggingFaceModelId>meta-llama/Llama-3.3-70B-Instruct</huggingFaceModelId>
     <huggingFaceTemperature>0.7</huggingFaceTemperature>
     <huggingFaceMaxNewTokens>50</huggingFaceMaxNewTokens>
     <huggingFaceReturnFullText>false</huggingFaceReturnFullText>
@@ -465,7 +465,7 @@ Claim: ${claim}</input_text>
     <parallelism>1</parallelism>
     <openAiBaseUrl>https://api.openai.com/v1</openAiBaseUrl>
     <openAiApiKey>${OPENAI_API_KEY}</openAiApiKey>
-    <openAiModelName>gpt-4o-mini</openAiModelName>
+    <openAiModelName>gpt-5.6-luna</openAiModelName>
     <openAiTemperature>0.7</openAiTemperature>
     <openAiMaxTokens>50</openAiMaxTokens>
     <openAiResponseFormat>text</openAiResponseFormat>
diff --git 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/anthropic.hpl
 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/anthropic.hpl
index 7c4d7de5b7..c5977ed17a 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/anthropic.hpl
+++ 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/anthropic.hpl
@@ -259,7 +259,7 @@ limitations under the License.
     <anthropicBaseUrl>https://api.anthropic.com/v1/</anthropicBaseUrl>
     <anthropicApiKey>${ANTHROPIC_API_KEY}</anthropicApiKey>
     <anthropicVersion>2023-06-01</anthropicVersion>
-    <anthropicModelName>claude-3-5-sonnet-20240620</anthropicModelName>
+    <anthropicModelName>claude-sonnet-5</anthropicModelName>
     <anthropicMaxTokens>1024</anthropicMaxTokens>
     <anthropicTimeout>15</anthropicTimeout>
     <anthropicMaxRetries>3</anthropicMaxRetries>
diff --git 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/huggingface.hpl
 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/huggingface.hpl
index 08de7ee2b1..9daac39521 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/huggingface.hpl
+++ 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/huggingface.hpl
@@ -82,7 +82,7 @@ limitations under the License.
     <openAiLogRequests>false</openAiLogRequests>
     <openAiLogResponses>false</openAiLogResponses>
     <huggingFaceAccessToken>${HF_TOKEN}</huggingFaceAccessToken>
-    <huggingFaceModelId>meta-llama/Llama-3.1-70B-Instruct</huggingFaceModelId>
+    <huggingFaceModelId>meta-llama/Llama-3.3-70B-Instruct</huggingFaceModelId>
     <huggingFaceTemperature>0.1</huggingFaceTemperature>
     <huggingFaceMaxNewTokens>50</huggingFaceMaxNewTokens>
     <huggingFaceReturnFullText>false</huggingFaceReturnFullText>
diff --git 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/openai.hpl
 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/openai.hpl
index 549ded8867..b9c3b79c84 100644
--- 
a/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/openai.hpl
+++ 
b/plugins/transforms/languagemodelchat/src/main/samples/transforms/languagemodelchat/zero-shot/openai.hpl
@@ -272,7 +272,7 @@ 
https://upload.wikimedia.org/wikipedia/commons/3/3d/Fesoj_-_Papilio_machaon_%28b
     <parallelism>1</parallelism>
     <openAiBaseUrl>https://api.openai.com/v1</openAiBaseUrl>
     <openAiApiKey>${OPENAI_API_KEY}</openAiApiKey>
-    <openAiModelName>gpt-4-turbo</openAiModelName>
+    <openAiModelName>gpt-5.6-luna</openAiModelName>
     <openAiTimeout>60</openAiTimeout>
     <openAiMaxRetries>3</openAiMaxRetries>
     <openAiUseProxy>false</openAiUseProxy>
diff --git 
a/plugins/transforms/languagemodelchat/src/test/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMetaSecretsTest.java
 
b/plugins/transforms/languagemodelchat/src/test/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMetaSecretsTest.java
new file mode 100644
index 0000000000..e54d938cc1
--- /dev/null
+++ 
b/plugins/transforms/languagemodelchat/src/test/java/org/apache/hop/pipeline/transforms/languagemodelchat/LanguageModelChatMetaSecretsTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.languagemodelchat;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.w3c.dom.Node;
+
+/** Api keys and access tokens are stored the way Hop stores every other 
secret. */
+class LanguageModelChatMetaSecretsTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  @BeforeAll
+  static void init() throws Exception {
+    HopClientEnvironment.init();
+  }
+
+  @Test
+  void keysAreNotWrittenInTheClear() throws Exception {
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setOpenAiApiKey("sk-open-ai-secret");
+    meta.setAnthropicApiKey("sk-anthropic-secret");
+    meta.setMistralApiKey("sk-mistral-secret");
+    meta.setHuggingFaceAccessToken("hf-secret");
+
+    String xml = meta.getXml();
+
+    assertFalse(xml.contains("sk-open-ai-secret"), xml);
+    assertFalse(xml.contains("sk-anthropic-secret"), xml);
+    assertFalse(xml.contains("sk-mistral-secret"), xml);
+    assertFalse(xml.contains("hf-secret"), xml);
+    assertTrue(xml.contains("<openAiApiKey>Encrypted "), xml);
+
+    LanguageModelChatMeta back = fromXml(xml);
+    assertEquals("sk-open-ai-secret", back.getOpenAiApiKey());
+    assertEquals("sk-anthropic-secret", back.getAnthropicApiKey());
+    assertEquals("sk-mistral-secret", back.getMistralApiKey());
+    assertEquals("hf-secret", back.getHuggingFaceAccessToken());
+  }
+
+  @Test
+  void variablesStayVariables() throws Exception {
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setOpenAiApiKey("${OPENAI_API_KEY}");
+    meta.setAnthropicApiKey("${ANTHROPIC_API_KEY}");
+    meta.setMistralApiKey("${MISTRAL_API_KEY}");
+    meta.setHuggingFaceAccessToken("${HF_ACCESS_TOKEN}");
+
+    String xml = meta.getXml();
+    assertTrue(xml.contains("<openAiApiKey>${OPENAI_API_KEY}</openAiApiKey>"), 
xml);
+
+    LanguageModelChatMeta back = fromXml(xml);
+    assertEquals("${OPENAI_API_KEY}", back.getOpenAiApiKey());
+    assertEquals("${ANTHROPIC_API_KEY}", back.getAnthropicApiKey());
+    assertEquals("${MISTRAL_API_KEY}", back.getMistralApiKey());
+    assertEquals("${HF_ACCESS_TOKEN}", back.getHuggingFaceAccessToken());
+  }
+
+  /** Pipelines written before the fields became passwords hold their value in 
the clear. */
+  @Test
+  void plainTextKeysFromOlderPipelinesStillLoad() throws Exception {
+    LanguageModelChatMeta back =
+        fromXml(
+            "<openAiApiKey>sk-written-by-an-older-hop</openAiApiKey>"
+                + "<mistralApiKey>${MISTRAL_API_KEY}</mistralApiKey>"
+                + 
"<huggingFaceAccessToken>HF_ACCESS_TOKEN</huggingFaceAccessToken>");
+
+    assertEquals("sk-written-by-an-older-hop", back.getOpenAiApiKey());
+    assertEquals("${MISTRAL_API_KEY}", back.getMistralApiKey());
+    assertEquals("HF_ACCESS_TOKEN", back.getHuggingFaceAccessToken());
+  }
+
+  /** The field default disagreed with setDefault() and shipped a stray brace, 
see issue #8336. */
+  @Test
+  void openAiApiKeyDefaultHasNoStrayBrace() {
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    assertEquals("OPENAI_API_KEY", meta.getOpenAiApiKey());
+
+    meta.setDefault();
+    assertEquals("OPENAI_API_KEY", meta.getOpenAiApiKey());
+  }
+
+  private LanguageModelChatMeta fromXml(String xml) throws Exception {
+    Node node =
+        XmlHandler.loadXmlString(
+            XmlHandler.openTag("transform") + xml + 
XmlHandler.closeTag("transform"), "transform");
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.loadXml(node, new MemoryMetadataProvider());
+    return meta;
+  }
+}
diff --git 
a/plugins/transforms/languagemodelchat/src/test/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacadeChatTest.java
 
b/plugins/transforms/languagemodelchat/src/test/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacadeChatTest.java
new file mode 100644
index 0000000000..25f8757a18
--- /dev/null
+++ 
b/plugins/transforms/languagemodelchat/src/test/java/org/apache/hop/pipeline/transforms/languagemodelchat/internals/LanguageModelFacadeChatTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.languagemodelchat.internals;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.sun.net.httpserver.HttpServer;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import 
org.apache.hop.pipeline.transforms.languagemodelchat.LanguageModelChatMeta;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/**
+ * Drives the providers against a stub http server, so the request 
serialization, the http call and
+ * the response parsing are all covered without reaching out to a provider.
+ */
+class LanguageModelFacadeChatTest {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
+  private final IVariables variables = new Variables();
+  private final AtomicReference<String> lastRequestBody = new 
AtomicReference<>();
+
+  private HttpServer server;
+  private String baseUrl;
+
+  @BeforeEach
+  void startServer() throws IOException {
+    server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+    baseUrl = "http://localhost:"; + server.getAddress().getPort();
+    server.start();
+  }
+
+  @AfterEach
+  void stopServer() {
+    server.stop(0);
+  }
+
+  /** Provider payloads are pretty printed, so requests are matched without 
their whitespace. */
+  private String compactRequestBody() {
+    return lastRequestBody.get().replaceAll("\\s+", "");
+  }
+
+  private void respondWith(String path, String body) {
+    server.createContext(
+        path,
+        exchange -> {
+          try (InputStream in = exchange.getRequestBody()) {
+            lastRequestBody.set(new String(in.readAllBytes(), UTF_8));
+          }
+          byte[] bytes = body.getBytes(UTF_8);
+          exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+          exchange.sendResponseHeaders(200, bytes.length);
+          exchange.getResponseBody().write(bytes);
+          exchange.close();
+        });
+  }
+
+  @Test
+  void openAi() throws Exception {
+    respondWith(
+        "/v1/chat/completions",
+        """
+        
{"id":"chat-1","object":"chat.completion","created":1,"model":"gpt-test",
+         
"choices":[{"index":0,"message":{"role":"assistant","content":"four"},"finish_reason":"stop"}],
+         "usage":{"prompt_tokens":11,"completion_tokens":22,"total_tokens":33}}
+        """);
+
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setModelType(ModelType.OPEN_AI.code());
+    meta.setOpenAiBaseUrl(baseUrl + "/v1");
+    meta.setOpenAiApiKey("test-key");
+    meta.setOpenAiModelName("gpt-test");
+    meta.setOpenAiResponseFormat("json");
+
+    ChatResponse response = chat(meta, "two plus two");
+
+    assertEquals("four", response.aiMessage().text());
+    assertEquals(11, response.tokenUsage().inputTokenCount());
+    assertEquals(22, response.tokenUsage().outputTokenCount());
+    assertEquals(33, response.tokenUsage().totalTokenCount());
+    assertTrue(compactRequestBody().contains("twoplustwo"), 
lastRequestBody.get());
+    assertTrue(
+        
compactRequestBody().contains("\"response_format\":{\"type\":\"json_object\"}"),
+        lastRequestBody.get());
+  }
+
+  @Test
+  void ollama() throws Exception {
+    respondWith(
+        "/api/chat",
+        """
+        {"model":"phi3","created_at":"2026-09-11T00:00:00Z",
+         "message":{"role":"assistant","content":"four"},
+         
"done":true,"done_reason":"stop","prompt_eval_count":11,"eval_count":22}
+        """);
+
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setModelType(ModelType.OLLAMA.code());
+    meta.setOllamaImageEndpoint(baseUrl);
+    meta.setOllamaModelName("phi3");
+    meta.setOllamaFormat("json");
+
+    ChatResponse response = chat(meta, "two plus two");
+
+    assertEquals("four", response.aiMessage().text());
+    assertEquals(11, response.tokenUsage().inputTokenCount());
+    assertTrue(compactRequestBody().contains("\"format\":\"json\""), 
lastRequestBody.get());
+  }
+
+  @Test
+  void anthropic() throws Exception {
+    respondWith(
+        "/v1/messages",
+        """
+        
{"id":"msg-1","type":"message","role":"assistant","model":"claude-test",
+         "content":[{"type":"text","text":"four"}],
+         
"stop_reason":"end_turn","usage":{"input_tokens":11,"output_tokens":22}}
+        """);
+
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setModelType(ModelType.ANTHROPIC.code());
+    meta.setAnthropicBaseUrl(baseUrl + "/v1/");
+    meta.setAnthropicApiKey("test-key");
+    meta.setAnthropicModelName("claude-test");
+
+    ChatResponse response = chat(meta, "two plus two");
+
+    assertEquals("four", response.aiMessage().text());
+    assertEquals(11, response.tokenUsage().inputTokenCount());
+    assertEquals(22, response.tokenUsage().outputTokenCount());
+  }
+
+  @Test
+  void mistral() throws Exception {
+    respondWith(
+        "/v1/chat/completions",
+        """
+        
{"id":"chat-1","object":"chat.completion","created":1,"model":"mistral-test",
+         
"choices":[{"index":0,"message":{"role":"assistant","content":"four"},"finish_reason":"stop"}],
+         "usage":{"prompt_tokens":11,"completion_tokens":22,"total_tokens":33}}
+        """);
+
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setModelType(ModelType.MISTRAL.code());
+    meta.setMistralBaseUrl(baseUrl + "/v1/");
+    meta.setMistralApiKey("test-key");
+    meta.setMistralModelName("mistral-test");
+    meta.setMistralResponseFormat("json_object");
+
+    ChatResponse response = chat(meta, "two plus two");
+
+    assertEquals("four", response.aiMessage().text());
+    assertEquals(33, response.tokenUsage().totalTokenCount());
+    assertTrue(
+        
compactRequestBody().contains("\"response_format\":{\"type\":\"json_object\"}"),
+        lastRequestBody.get());
+  }
+
+  @Test
+  void huggingFaceDedicatedEndpoint() throws Exception {
+    respondWith("/", """
+        [{"generated_text":"four"}]
+        """);
+
+    LanguageModelChatMeta meta = new LanguageModelChatMeta();
+    meta.setModelType(ModelType.HUGGING_FACE.code());
+    meta.setHuggingFaceModelId(baseUrl + "/");
+    meta.setHuggingFaceAccessToken("test-token");
+    meta.setHuggingFaceMaxNewTokens(10);
+
+    ChatResponse response = chat(meta, "two plus two");
+
+    assertEquals("four", response.aiMessage().text());
+    assertTrue(compactRequestBody().contains("\"max_new_tokens\":10"), 
lastRequestBody.get());
+    assertTrue(compactRequestBody().contains("\"wait_for_model\":true"), 
lastRequestBody.get());
+  }
+
+  private ChatResponse chat(LanguageModelChatMeta meta, String message) throws 
Exception {
+    LanguageModelFacade facade = new LanguageModelFacade(variables, meta);
+    return 
facade.chat(List.of(dev.langchain4j.data.message.UserMessage.userMessage(message)));
+  }
+}

Reply via email to