diff --git a/ai/README.md b/ai/README.md
new file mode 100644
index 000000000..2734ad945
--- /dev/null
+++ b/ai/README.md
@@ -0,0 +1,136 @@
+# AI Plugin for Apache OFBiz
+
+LangChain4j integration that exposes AI/LLM capabilities as standard OFBiz services.
+
+Apache JIRA: https://issues.apache.org/jira/browse/OFBIZ-13408
+
+## What this plugin does
+
+The AI plugin connects OFBiz to any OpenAI-compatible chat model via LangChain4j 1.8.0.
+It provides two callable OFBiz services — `aiGenerate` for free-text responses and
+`aiGenerateStructured` for JSON-schema-constrained structured output — that any other
+service or Groovy script can call without depending on LangChain4j directly.
+
+## Architecture
+
+| Layer | Class | Role |
+|---|---|---|
+| Container | `AiContainer` | Reads `ai.properties` at startup, builds the `ChatModel`, calls `AiFactory.setChatModel()` |
+| Singleton | `AiFactory` | Holds the live `ChatModel` instance; throws `IllegalStateException` if not initialized |
+| Utility | `AiWorker` | Static `generate()` and `generateStructured()` methods; handles message conversion and JSON schema mapping |
+| Services | `AiServices` | Thin OFBiz service wrappers that delegate to `AiWorker` and return standard service result maps |
+
+## Prerequisites
+
+- Java 17 or later
+- OFBiz trunk
+- An API key from OpenAI or a compatible provider (Ollama, Groq, Together, Azure OpenAI)
+
+## Installation
+
+1. The plugin is already placed at `plugins/ai/` inside the OFBiz source tree.
+2. Copy the properties template and fill in your values:
+ ```
+ cp plugins/ai/config/ai.properties.template plugins/ai/config/ai.properties
+ ```
+ If no template exists, create `plugins/ai/config/ai.properties` from the table below.
+3. Start OFBiz normally. The container will log:
+ ```
+ AI plugin initialized: provider=openai model=gpt-4o-mini
+ ```
+ If the API key is missing or placeholder, startup continues but the plugin logs an error and skips initialization.
+
+## Configuration
+
+Edit `plugins/ai/config/ai.properties` (this file is gitignored — never commit API keys).
+
+| Property | Description | Default |
+|---|---|---|
+| `ai.provider` | Provider name. Currently used for logging; the `openai` engine handles all OpenAI-compatible endpoints. | `openai` |
+| `ai.model` | Model name passed to the provider. | `gpt-4o-mini` |
+| `ai.baseUrl` | Base URL override. Leave empty for the OpenAI default. Set for local or third-party endpoints. | _(empty)_ |
+| `ai.apiKey` | Your API key. **Required.** | _(none)_ |
+| `ai.timeout` | Request timeout in seconds. | `60` |
+
+## Multiple providers
+
+Setting `ai.baseUrl` makes the plugin work with any OpenAI-compatible endpoint:
+
+| Provider | `ai.baseUrl` |
+|---|---|
+| OpenAI (default) | _(leave empty)_ |
+| Ollama | `http://localhost:11434` |
+| Groq | `https://api.groq.com/openai/v1` |
+| Together AI | `https://api.together.xyz/v1` |
+| Azure OpenAI | your Azure endpoint URL |
+
+## Usage
+
+### Generate free-text (from a Groovy service)
+
+```groovy
+import org.apache.ofbiz.ai.AiWorker
+
+def messages = [
+ [role: "system", content: "You are a helpful assistant."],
+ [role: "user", content: "Summarize this order in one sentence."]
+]
+
+String response = AiWorker.generate(dctx, messages)
+```
+
+### Generate structured output
+
+Schema values can be a plain type string (`"string"`, `"number"`, `"integer"`, `"boolean"`,
+`"array"`, `"object"`) or a map with `type` and optional `properties`/`items` for nested shapes.
+
+```groovy
+import org.apache.ofbiz.ai.AiWorker
+
+def messages = [
+ [role: "user", content: "Extract the product name and price from: 'Widget Pro costs \$49.99'"]
+]
+
+def schema = [
+ productName: "string",
+ price: "number"
+]
+
+Map result = AiWorker.generateStructured(dctx, messages, schema)
+// result == [productName: "Widget Pro", price: 49.99]
+```
+
+Both methods throw `GeneralException` on failure; callers should catch it and return
+`ServiceUtil.returnError()` as appropriate.
+
+## Available services
+
+| Service | IN | OUT | Description |
+|---|---|---|---|
+| `aiGenerate` | `messages` (List, required)
`configName` (String, optional) | `response` (String) | Free-text generation |
+| `aiGenerateStructured` | `messages` (List, required)
`schema` (Map, required)
`configName` (String, optional) | `result` (Map) | Structured JSON output |
+
+Each message in the `messages` list is a `Map` with keys `role` (`system`, `user`, or `assistant`) and `content` (String).
+
+## Smoke test
+
+With OFBiz running and a valid API key configured, invoke `aiSmokeTest` from the
+webtools service runner:
+
+```
+https://localhost:8443/webtools/control/main
+→ Service Engine → Run Service → aiSmokeTest
+```
+
+A successful run logs:
+```
+AI smoke test response: Hello
+```
+
+## Adding new providers
+
+To support a provider that is not OpenAI-compatible (e.g., Anthropic native, Amazon Bedrock),
+add a new `case` to the `switch (provider)` block in `AiContainer.java`. Instantiate the
+provider's LangChain4j `ChatModel` builder, call `AiFactory.setChatModel(chatModel)`, and
+add the corresponding `dev.langchain4j:langchain4j-` dependency to `build.gradle`.
+No changes to `AiFactory`, `AiWorker`, or `AiServices` are needed.
diff --git a/ai/build.gradle b/ai/build.gradle
new file mode 100644
index 000000000..91ae11591
--- /dev/null
+++ b/ai/build.gradle
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+dependencies {
+ pluginLibsCompile 'dev.langchain4j:langchain4j:1.8.0'
+ pluginLibsCompile 'dev.langchain4j:langchain4j-anthropic:1.8.0'
+ pluginLibsCompile 'dev.langchain4j:langchain4j-ollama:1.8.0'
+ pluginLibsCompile 'dev.langchain4j:langchain4j-open-ai:1.8.0'
+}
diff --git a/ai/config/ai.properties b/ai/config/ai.properties
new file mode 100644
index 000000000..bcdfb2598
--- /dev/null
+++ b/ai/config/ai.properties
@@ -0,0 +1,38 @@
+# AI Plugin configuration
+# This file is gitignored — never commit API keys.
+#
+# Supported providers: openai | anthropic | ollama
+# To switch provider: uncomment the desired block, comment out the others.
+
+# -------------------------------------------------
+# Provider: openai
+# Models: gpt-4o, gpt-4o-mini, gpt-4-turbo, o1-mini
+# Docs: https://platform.openai.com/docs/models
+# -------------------------------------------------
+ai.provider=openai
+ai.model=gpt-4o-mini
+ai.apiKey=REPLACE_WITH_YOUR_API_KEY
+ai.baseUrl=
+ai.timeout=60
+
+# -------------------------------------------------
+# Provider: anthropic
+# Models: claude-opus-4-5, claude-sonnet-4-5, claude-3-5-haiku-20241022
+# Docs: https://docs.anthropic.com/en/docs/about-claude/models
+# -------------------------------------------------
+#ai.provider=anthropic
+#ai.model=claude-3-5-haiku-20241022
+#ai.apiKey=REPLACE_WITH_YOUR_API_KEY
+#ai.baseUrl=
+#ai.timeout=60
+
+# -------------------------------------------------
+# Provider: ollama (local, no API key required)
+# Models: llama3.2, llama3.1, mistral, gemma3
+# Docs: https://ollama.com/library
+# -------------------------------------------------
+#ai.provider=ollama
+#ai.model=llama3.2
+#ai.apiKey=
+#ai.baseUrl=http://localhost:11434
+#ai.timeout=120
diff --git a/ai/groovyScripts/AiStructuredTest.groovy b/ai/groovyScripts/AiStructuredTest.groovy
new file mode 100644
index 000000000..2c4cc6d53
--- /dev/null
+++ b/ai/groovyScripts/AiStructuredTest.groovy
@@ -0,0 +1,25 @@
+import org.apache.ofbiz.base.util.Debug
+import org.apache.ofbiz.ai.AiWorker
+
+final String MODULE = 'AiStructuredTest.groovy'
+
+def messages = [
+ [role: "user", content: "Return a greeting with a single word."]
+]
+
+def schema = [
+ word: "string"
+]
+
+try {
+ Map result = AiWorker.generateStructured(dctx, messages, schema)
+ if (!result || !result.containsKey("word")) {
+ Debug.logError("AI structured smoke test failed: response missing 'word' key. Got: " + result, MODULE)
+ return error("AI structured smoke test failed: missing 'word' key in response")
+ }
+ Debug.logInfo("AI structured smoke test response: " + result, MODULE)
+ return success("AI structured smoke test passed: " + result)
+} catch (Exception e) {
+ Debug.logError(e, "AI structured smoke test failed", MODULE)
+ return error("AI structured smoke test failed: " + e.getMessage())
+}
diff --git a/ai/groovyScripts/AiTest.groovy b/ai/groovyScripts/AiTest.groovy
new file mode 100644
index 000000000..9a8597b40
--- /dev/null
+++ b/ai/groovyScripts/AiTest.groovy
@@ -0,0 +1,17 @@
+import org.apache.ofbiz.base.util.Debug
+import org.apache.ofbiz.ai.AiWorker
+
+final String MODULE = 'AiTest.groovy'
+
+def messages = [
+ [role: "user", content: "Say hello in one word."]
+]
+
+try {
+ String response = AiWorker.generate(dctx, messages)
+ Debug.logInfo("AI smoke test response: " + response, MODULE)
+ return success("AI smoke test passed: " + response)
+} catch (Exception e) {
+ Debug.logError(e, "AI smoke test failed", MODULE)
+ return error("AI smoke test failed: " + e.getMessage())
+}
diff --git a/ai/ofbiz-component.xml b/ai/ofbiz-component.xml
new file mode 100644
index 000000000..6f4a3539d
--- /dev/null
+++ b/ai/ofbiz-component.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ai/servicedef/services.xml b/ai/servicedef/services.xml
new file mode 100644
index 000000000..f98c7e374
--- /dev/null
+++ b/ai/servicedef/services.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+ Generate a text response from an AI model
+
+
+
+
+
+
+ Generate a structured Map response from an AI model
+
+
+
+
+
+
+
+ Smoke test for the AI plugin — calls aiGenerate with a test message
+
+
+
+ Smoke test for generateStructured — expects Map with word key
+
+
+
diff --git a/ai/src/main/java/org/apache/ofbiz/ai/AiServices.java b/ai/src/main/java/org/apache/ofbiz/ai/AiServices.java
new file mode 100644
index 000000000..47fb8207d
--- /dev/null
+++ b/ai/src/main/java/org/apache/ofbiz/ai/AiServices.java
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * 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.ofbiz.ai;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.GeneralException;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.service.DispatchContext;
+import org.apache.ofbiz.service.ServiceUtil;
+
+public class AiServices {
+
+ private static final String MODULE = AiServices.class.getName();
+
+ public static Map generate(DispatchContext dctx, Map context) {
+ List