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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12194-b0f6feddf9ed0dc1f5d485cfc69d387edfe6ac82
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit 16ab3a75a9f38d7229ec9751b1c96fef79f081bc
Author: Goutam Adwant <[email protected]>
AuthorDate: Tue Oct 13 16:00:53 2026 +0000

    [Improve][Transform-V2] Add opt-in validation for LLM boolean output 
(#12194)
    
    Signed-off-by: Goutam Adwant <[email protected]>
---
 .licenserc.yaml                                    |   2 +
 docs/en/transforms/llm.md                          |  19 +
 docs/zh/transforms/llm.md                          |  16 +
 .../apache/seatunnel/e2e/transform/TestLLMIT.java  |  19 +
 .../llm_openai_transform_boolean_strict.conf       |  74 ++++
 ...lm_openai_transform_boolean_strict_invalid.conf |  74 ++++
 .../transform/nlpmodel/llm/LLMTransform.java       |  40 ++-
 .../transform/nlpmodel/llm/LLMTransformConfig.java |   9 +
 .../nlpmodel/llm/LLMTransformFactory.java          |   1 +
 .../transform/llm/LLMBooleanOutputTest.java        | 383 +++++++++++++++++++++
 .../resources/llm/GenerateLegacyLLMFixture.java    |  80 +++++
 .../src/test/resources/llm/README.md               |  56 +++
 .../test/resources/llm/generate-legacy-fixture.sh  |  40 +++
 .../resources/llm/legacy-boolean-wrapper.base64    |  74 ++++
 14 files changed, 884 insertions(+), 3 deletions(-)

diff --git a/.licenserc.yaml b/.licenserc.yaml
index 079dca2362..222c81e3c0 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -44,6 +44,8 @@ header:
     - '**/NOTICE'
     - '**/.gitkeep'
     - '**/com/typesafe/config/**'
+    # Archived serialization bytes; ASF license and provenance are in the 
adjacent README.md.
+    - 
'seatunnel-transforms-v2/src/test/resources/llm/legacy-boolean-wrapper.base64'
     - 
'seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/ConfigProvider.java'
     - 
'seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/SeaTunnelConfigSections.java'
     - 
'seatunnel-engine/seatunnel-engine-common/src/main/java/org/apache/seatunnel/engine/common/config/YamlSeaTunnelConfigBuilder.java'
diff --git a/docs/en/transforms/llm.md b/docs/en/transforms/llm.md
index 62c1396318..8609e54d02 100644
--- a/docs/en/transforms/llm.md
+++ b/docs/en/transforms/llm.md
@@ -14,6 +14,7 @@ more.
 |------------------------|--------|----------|---------------|
 | model_provider         | enum   | yes      |               |
 | output_data_type       | enum   | no       | String        |
+| strict_boolean_output  | boolean | no      | false         |
 | output_column_name     | string | no       | llm_output    |
 | prompt                 | string | yes      |               |
 | inference_columns      | list   | no       |               |
@@ -38,6 +39,24 @@ The data type of the output data. The available options are:
 STRING,INT,BIGINT,DOUBLE,BOOLEAN.
 Default value is STRING.
 
+### strict_boolean_output
+
+Optional validation for `output_data_type = BOOLEAN`. The default is `false` 
and preserves
+the existing conversion: only `true` (case-insensitive) becomes `true`; other 
values,
+including an empty string or a null element, become `false`.
+
+Set `strict_boolean_output = true` to require exactly one non-null `true` or 
`false`
+value from the model for each input row. Matching is case-insensitive and does 
not trim
+whitespace. Values such as `"unknown"`, `""`, `" true "`, `1`, null, an empty 
result, or
+multiple results fail the transform instead of producing a boolean value. The 
validation
+error does not include the input row or model response. This option does not 
add row
+skipping or error-table routing, and has no effect on other output data types.
+
+```hocon
+output_data_type = BOOLEAN
+strict_boolean_output = true
+```
+
 ### output_column_name
 
 Custom output data field name. It must not duplicate an existing input field 
name, otherwise the transform fails with the error `llm inference field name 
... already exists`.
diff --git a/docs/zh/transforms/llm.md b/docs/zh/transforms/llm.md
index 6498450f7a..b342a83f08 100644
--- a/docs/zh/transforms/llm.md
+++ b/docs/zh/transforms/llm.md
@@ -12,6 +12,7 @@ LLM 转换插件利用大型语言模型(LLM)的能力处理数据,将输
 |------------------------| ------ | -------- |-------------|
 | model_provider         | enum   | yes      |             |
 | output_data_type       | enum   | no       | String      |
+| strict_boolean_output  | boolean | no      | false       |
 | output_column_name     | string | no       | llm_output   |
 | prompt                 | string | yes      |             |
 | inference_columns      | list   | no       |             |
@@ -36,6 +37,21 @@ OPENAI,DOUBAO,DEEPSEEK,KIMIAI,MICROSOFT, ZHIPU, CUSTOM
 STRING,INT,BIGINT,DOUBLE,BOOLEAN.
 默认值为 STRING。
 
+### strict_boolean_output
+
+`output_data_type = BOOLEAN` 时的可选校验。默认值为 `false`,保持现有转换行为:
+只有 `true`(不区分大小写)转换为 `true`;其他值(包括空字符串和 null 元素)转换为 `false`。
+
+设置 `strict_boolean_output = true` 后,每条输入记录对应的模型结果必须且只能包含一个非 null 的
+`true` 或 `false` 值。匹配不区分大小写,但不会去除首尾空白。`"unknown"`、`""`、`" true "`、
+`1`、null、空结果或多个结果都会使转换失败,而不是输出布尔值。校验错误不会包含输入记录或模型响应。
+该选项不增加跳过记录或路由到错误表的功能,对其他输出数据类型没有影响。
+
+```hocon
+output_data_type = BOOLEAN
+strict_boolean_output = true
+```
+
 ### output_column_name
 
 自定义输出数据字段名称。该名称不能与输入数据中已有的字段名重复,否则转换插件会抛出错误:`llm inference field name ... 
already exists`。
diff --git 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestLLMIT.java
 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestLLMIT.java
index 495e2bd714..3a936a7c89 100644
--- 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestLLMIT.java
+++ 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestLLMIT.java
@@ -111,6 +111,25 @@ public class TestLLMIT extends TestSuiteBase implements 
TestResource {
         Assertions.assertEquals(0, execResult.getExitCode());
     }
 
+    @TestTemplate
+    public void testLLMWithOpenAIStrictBoolean(TestContainer container)
+            throws IOException, InterruptedException {
+        Container.ExecResult execResult =
+                
container.executeJob("/llm_openai_transform_boolean_strict.conf");
+        Assertions.assertEquals(0, execResult.getExitCode());
+    }
+
+    @TestTemplate
+    public void 
testLLMWithOpenAIStrictBooleanRejectsInvalidOutput(TestContainer container)
+            throws IOException, InterruptedException {
+        Container.ExecResult execResult =
+                
container.executeJob("/llm_openai_transform_boolean_strict_invalid.conf");
+        Assertions.assertNotEquals(0, execResult.getExitCode());
+        String output = execResult.getStdout() + execResult.getStderr();
+        Assertions.assertTrue(output.contains("strict_boolean_output"), 
output);
+        Assertions.assertTrue(output.contains("exactly one non-null true or 
false"), output);
+    }
+
     @TestTemplate
     public void testLLMWithOpenAIColumns(TestContainer container)
             throws IOException, InterruptedException {
diff --git 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/llm_openai_transform_boolean_strict.conf
 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/llm_openai_transform_boolean_strict.conf
new file mode 100644
index 0000000000..bd2f517921
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/llm_openai_transform_boolean_strict.conf
@@ -0,0 +1,74 @@
+#
+# 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.
+#
+
+env {
+  job.mode = "BATCH"
+}
+
+source {
+  FakeSource {
+    row.num = 5
+    schema = {
+      fields {
+        id = "int"
+        name = "string"
+      }
+    }
+    rows = [
+      {fields = [1, "Jia Fan"], kind = INSERT}
+      {fields = [2, "Hailin Wang"], kind = INSERT}
+      {fields = [3, "Tomas"], kind = INSERT}
+      {fields = [4, "Eric"], kind = INSERT}
+      {fields = [5, "Guangdong Liu"], kind = INSERT}
+    ]
+    plugin_output = "fake"
+  }
+}
+
+transform {
+  LLM {
+    plugin_input = "fake"
+    model_provider = OPENAI
+    model = gpt-4o-mini
+    api_key = sk-xxx
+    prompt = "Determine whether someone is Chinese or American by their name"
+    output_data_type = boolean
+    strict_boolean_output = true
+    api_path = "http://mockserver:1080/v2/chat/completions";
+    plugin_output = "llm_output"
+  }
+}
+
+sink {
+  Assert {
+    plugin_input = "llm_output"
+    rules =
+      {
+        field_rules = [
+          {
+            field_name = llm_output
+            field_type = boolean
+            field_value = [
+              {
+                rule_type = NOT_NULL
+              }
+            ]
+          }
+        ]
+      }
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/llm_openai_transform_boolean_strict_invalid.conf
 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/llm_openai_transform_boolean_strict_invalid.conf
new file mode 100644
index 0000000000..0054d702a0
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/llm_openai_transform_boolean_strict_invalid.conf
@@ -0,0 +1,74 @@
+#
+# 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.
+#
+
+env {
+  job.mode = "BATCH"
+}
+
+source {
+  FakeSource {
+    row.num = 5
+    schema = {
+      fields {
+        id = "int"
+        name = "string"
+      }
+    }
+    rows = [
+      {fields = [1, "Jia Fan"], kind = INSERT}
+      {fields = [2, "Hailin Wang"], kind = INSERT}
+      {fields = [3, "Tomas"], kind = INSERT}
+      {fields = [4, "Eric"], kind = INSERT}
+      {fields = [5, "Guangdong Liu"], kind = INSERT}
+    ]
+    plugin_output = "fake"
+  }
+}
+
+transform {
+  LLM {
+    plugin_input = "fake"
+    model_provider = OPENAI
+    model = gpt-4o-mini
+    api_key = sk-xxx
+    prompt = "Determine whether someone is Chinese or American by their name"
+    output_data_type = boolean
+    strict_boolean_output = true
+    api_path = "http://mockserver:1080/v1/chat/completions";
+    plugin_output = "llm_output"
+  }
+}
+
+sink {
+  Assert {
+    plugin_input = "llm_output"
+    rules =
+      {
+        field_rules = [
+          {
+            field_name = llm_output
+            field_type = boolean
+            field_value = [
+              {
+                rule_type = NOT_NULL
+              }
+            ]
+          }
+        ]
+      }
+  }
+}
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransform.java
index 8160cdc647..b1750070df 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransform.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransform.java
@@ -25,7 +25,9 @@ import 
org.apache.seatunnel.api.table.catalog.SeaTunnelDataTypeConvertorUtil;
 import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.table.type.SeaTunnelRowAccessor;
+import org.apache.seatunnel.api.table.type.SqlType;
 import org.apache.seatunnel.transform.common.SingleFieldOutputTransform;
+import org.apache.seatunnel.transform.exception.TransformCommonError;
 import org.apache.seatunnel.transform.nlpmodel.ModelProvider;
 import org.apache.seatunnel.transform.nlpmodel.ModelTransformConfig;
 import org.apache.seatunnel.transform.nlpmodel.llm.remote.Model;
@@ -42,8 +44,13 @@ import java.util.Collections;
 import java.util.List;
 
 public class LLMTransform extends SingleFieldOutputTransform {
+    // Implicit UID at 8bea8c681cacccbd99fa642ca0853718980b9234, before 
strictBooleanOutput.
+    // Reproduce with serialver as documented in 
src/test/resources/llm/README.md.
+    private static final long serialVersionUID = 4711686225005641485L;
+
     private final ReadonlyConfig config;
     private final SeaTunnelDataType<?> outputDataType;
+    private final boolean strictBooleanOutput;
     private Model model;
 
     public LLMTransform(@NonNull ReadonlyConfig config, @NonNull CatalogTable 
inputCatalogTable) {
@@ -52,6 +59,7 @@ public class LLMTransform extends SingleFieldOutputTransform {
         this.outputDataType =
                 SeaTunnelDataTypeConvertorUtil.deserializeSeaTunnelDataType(
                         "output", 
config.get(LLMTransformConfig.OUTPUT_DATA_TYPE).toString());
+        this.strictBooleanOutput = 
config.get(LLMTransformConfig.STRICT_BOOLEAN_OUTPUT);
     }
 
     private void tryOpen() {
@@ -141,8 +149,17 @@ public class LLMTransform extends 
SingleFieldOutputTransform {
     protected Object getOutputFieldValue(SeaTunnelRowAccessor inputRow) {
         tryOpen();
         SeaTunnelRow seaTunnelRow = new SeaTunnelRow(inputRow.getFields());
+        List<String> values;
+        try {
+            values = model.inference(Collections.singletonList(seaTunnelRow));
+        } catch (Exception e) {
+            throw inferenceFailure(seaTunnelRow, e);
+        }
+        // Keep validation outside legacy error wrapping so rejected output 
does not expose the row.
+        if (strictBooleanOutput && outputDataType.getSqlType() == 
SqlType.BOOLEAN) {
+            return parseStrictBooleanOutput(values);
+        }
         try {
-            List<String> values = 
model.inference(Collections.singletonList(seaTunnelRow));
             switch (outputDataType.getSqlType()) {
                 case STRING:
                     return String.valueOf(values.get(0));
@@ -159,11 +176,28 @@ public class LLMTransform extends 
SingleFieldOutputTransform {
                             "Unsupported output data type: " + outputDataType);
             }
         } catch (Exception e) {
-            throw new RuntimeException(
-                    String.format("Failed to inference model with row %s", 
seaTunnelRow), e);
+            throw inferenceFailure(seaTunnelRow, e);
         }
     }
 
+    private boolean parseStrictBooleanOutput(List<String> values) {
+        String value = values != null && values.size() == 1 ? values.get(0) : 
null;
+        if ("true".equalsIgnoreCase(value)) {
+            return true;
+        }
+        if ("false".equalsIgnoreCase(value)) {
+            return false;
+        }
+        throw TransformCommonError.validationFailed(
+                "LLM strict_boolean_output requires exactly one non-null true 
or false result "
+                        + "for output_data_type=BOOLEAN, without surrounding 
whitespace");
+    }
+
+    private RuntimeException inferenceFailure(SeaTunnelRow row, Exception 
cause) {
+        return new RuntimeException(
+                String.format("Failed to inference model with row %s", row), 
cause);
+    }
+
     @Override
     protected Column getOutputColumn() {
         String customFieldName = 
config.get(LLMTransformConfig.OUTPUT_COLUMN_NAME);
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformConfig.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformConfig.java
index b26e4791ce..e87db6e3f5 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformConfig.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformConfig.java
@@ -43,6 +43,15 @@ public class LLMTransformConfig extends ModelTransformConfig 
{
                     .defaultValue("llm_output")
                     .withDescription("custom field name for the llm output 
data");
 
+    public static final Option<Boolean> STRICT_BOOLEAN_OUTPUT =
+            Options.key("strict_boolean_output")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription(
+                            "Require exactly one non-null true or false result 
for BOOLEAN output. "
+                                    + "Matching is case-insensitive and does 
not trim whitespace. "
+                                    + "The default false preserves legacy 
boolean conversion.");
+
     public static final Option<Integer> INFERENCE_BATCH_SIZE =
             Options.key("inference_batch_size")
                     .intType()
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
index 67f855b4c6..74c7ba0632 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
@@ -46,6 +46,7 @@ public class LLMTransformFactory implements 
TableTransformFactory {
                 .optional(
                         LLMTransformConfig.API_PATH,
                         LLMTransformConfig.OUTPUT_DATA_TYPE,
+                        LLMTransformConfig.STRICT_BOOLEAN_OUTPUT,
                         LLMTransformConfig.PROCESS_BATCH_SIZE)
                 .conditional(
                         LLMTransformConfig.MODEL_PROVIDER,
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/llm/LLMBooleanOutputTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/llm/LLMBooleanOutputTest.java
new file mode 100644
index 0000000000..b341ee3d41
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/llm/LLMBooleanOutputTest.java
@@ -0,0 +1,383 @@
+/*
+ * 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.seatunnel.transform.llm;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.ObjectMapper;
+import 
org.apache.seatunnel.shade.com.fasterxml.jackson.databind.node.ObjectNode;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import org.apache.seatunnel.api.table.catalog.TablePath;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.api.table.factory.TableTransformFactoryContext;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.RowKind;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.transform.SeaTunnelMapTransform;
+import org.apache.seatunnel.common.utils.SerializationUtils;
+import org.apache.seatunnel.transform.exception.TransformException;
+import org.apache.seatunnel.transform.nlpmodel.llm.LLMTransform;
+import org.apache.seatunnel.transform.nlpmodel.llm.LLMTransformFactory;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+@Timeout(30)
+class LLMBooleanOutputTest {
+
+    private static final String INPUT_VALUE = "sensitive-row-value";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private MockWebServer server;
+    private LLMTransform transform;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        server = new MockWebServer();
+        server.start();
+    }
+
+    @AfterEach
+    void tearDown() throws IOException {
+        try {
+            if (transform != null) {
+                transform.close();
+            }
+        } finally {
+            server.shutdown();
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("legacyBooleanOutputs")
+    void preservesDefaultBooleanConversion(String response, boolean expected) 
throws IOException {
+        configure("BOOLEAN", null);
+        Assertions.assertEquals(expected, mapResponse(response).getField(1));
+    }
+
+    static Stream<Arguments> legacyBooleanOutputs() {
+        return Stream.of(
+                Arguments.of("[\"unknown\"]", false),
+                Arguments.of("[\"\"]", false),
+                Arguments.of("[null]", false),
+                Arguments.of("[\" true \"]", false),
+                Arguments.of("[\"yes\"]", false),
+                Arguments.of("[1]", false),
+                Arguments.of("[\"TRUE\"]", true),
+                Arguments.of("[false]", false),
+                Arguments.of("[false, true]", false));
+    }
+
+    @Test
+    void explicitlyDisabledValidationPreservesCoercion() throws IOException {
+        configure("BOOLEAN", false);
+        Assertions.assertEquals(false, 
mapResponse("[\"unknown\"]").getField(1));
+    }
+
+    @ParameterizedTest
+    @MethodSource("validBooleanOutputs")
+    void acceptsBooleanLiteralsAndPreservesCaseCompatibility(String response, 
boolean expected)
+            throws IOException {
+        configure("BOOLEAN", true);
+        Assertions.assertEquals(expected, mapResponse(response).getField(1));
+    }
+
+    static Stream<Arguments> validBooleanOutputs() {
+        return Stream.of(
+                Arguments.of("[true]", true),
+                Arguments.of("[false]", false),
+                Arguments.of("[True]", true),
+                Arguments.of("[FALSE]", false),
+                Arguments.of("[\"TrUe\"]", true),
+                Arguments.of("[\"FaLsE\"]", false));
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "[\"unknown-private-response\"]",
+                "[\"\"]",
+                "[null]",
+                "[\" true \"]",
+                "[\"false \"]",
+                "[\"yes\"]",
+                "[0]",
+                "[]",
+                "null",
+                "[true, false]"
+            })
+    void strictValidationFailsWithoutExposingRowOrResponse(String response) 
throws IOException {
+        configure("BOOLEAN", true);
+        TransformException error =
+                Assertions.assertThrows(TransformException.class, () -> 
mapResponse(response));
+        
Assertions.assertTrue(error.getMessage().contains("strict_boolean_output"));
+        Assertions.assertTrue(error.getMessage().contains("true or false"));
+        StringWriter stackTrace = new StringWriter();
+        error.printStackTrace(new PrintWriter(stackTrace));
+        Assertions.assertFalse(stackTrace.toString().contains(INPUT_VALUE));
+        
Assertions.assertFalse(stackTrace.toString().contains("unknown-private-response"));
+        Assertions.assertNull(error.getCause());
+        Assertions.assertEquals(1, server.getRequestCount());
+    }
+
+    @ParameterizedTest
+    @MethodSource("nonBooleanOutputs")
+    void validationDoesNotChangeOtherOutputTypes(String type, String response, 
Object expected)
+            throws IOException {
+        configure(type, true);
+        Assertions.assertEquals(expected, mapResponse(response).getField(1));
+    }
+
+    static Stream<Arguments> nonBooleanOutputs() {
+        return Stream.of(
+                Arguments.of("STRING", "[\"unknown\"]", "unknown"),
+                Arguments.of("STRING", "[null]", "null"),
+                Arguments.of("STRING", "[\"first\", \"second\"]", "first"),
+                Arguments.of("INT", "[42]", 42),
+                Arguments.of("BIGINT", "[2147483648]", 2147483648L),
+                Arguments.of("DOUBLE", "[0.25]", 0.25D));
+    }
+
+    @Test
+    void preservesNumericConversionFailure() throws IOException {
+        configure("INT", true);
+        RuntimeException error =
+                Assertions.assertThrows(
+                        RuntimeException.class, () -> 
mapResponse("[\"invalid-number\"]"));
+        Assertions.assertInstanceOf(NumberFormatException.class, 
error.getCause());
+    }
+
+    @Test
+    void preservesSchemaAndRowMetadata() throws IOException {
+        configure("BOOLEAN", true);
+        Assertions.assertEquals(
+                BasicType.BOOLEAN_TYPE,
+                transform
+                        .getProducedCatalogTable()
+                        .getTableSchema()
+                        .getColumns()
+                        .get(1)
+                        .getDataType());
+        SeaTunnelRow output = mapResponse("[true]");
+        Assertions.assertEquals(INPUT_VALUE, output.getField(0));
+        Assertions.assertEquals("test.input", output.getTableId());
+        Assertions.assertEquals(RowKind.UPDATE_AFTER, output.getRowKind());
+    }
+
+    @Test
+    void exposesOptionalValidationInFactoryRule() {
+        Assertions.assertTrue(
+                new LLMTransformFactory()
+                        .optionRule().getOptionalOptions().stream()
+                                .anyMatch(option -> 
"strict_boolean_output".equals(option.key())));
+    }
+
+    @Test
+    void preservesLegacySerializationIdentifier() {
+        Assertions.assertEquals(
+                4711686225005641485L,
+                
ObjectStreamClass.lookup(LLMTransform.class).getSerialVersionUID());
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void readsLegacyFactoryWrapperWithValidationDisabled() throws Exception {
+        try (InputStream resource =
+                
getClass().getResourceAsStream("/llm/legacy-boolean-wrapper.base64")) {
+            Assertions.assertNotNull(resource);
+            try (ObjectInputStream input =
+                    new 
ObjectInputStream(Base64.getMimeDecoder().wrap(resource)) {
+                        {
+                            enableResolveObject(true);
+                        }
+
+                        @Override
+                        protected Object resolveObject(Object value) {
+                            if 
("http://127.0.0.1:1/legacy-fixture".equals(value)) {
+                                // Only relocate the HTTP endpoint; leave 
serialized fields intact.
+                                return 
server.url("/chat/completions").toString();
+                            }
+                            if (value instanceof LLMTransform) {
+                                transform = (LLMTransform) value;
+                            }
+                            return value;
+                        }
+                    }) {
+                SeaTunnelMapTransform<SeaTunnelRow> restored =
+                        (SeaTunnelMapTransform<SeaTunnelRow>) 
input.readObject();
+                Assertions.assertNotNull(transform);
+                Assertions.assertEquals(false, mapResponse("[\"unknown\"]", 
restored).getField(1));
+                Assertions.assertEquals(true, mapResponse("[\"TrUe\"]", 
restored).getField(1));
+                SeaTunnelRow output = mapResponse("[\"FaLsE\"]", restored);
+                Assertions.assertEquals(false, output.getField(1));
+                Assertions.assertEquals(INPUT_VALUE, output.getField(0));
+                Assertions.assertEquals("test.input", output.getTableId());
+                Assertions.assertEquals(RowKind.UPDATE_AFTER, 
output.getRowKind());
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    @SuppressWarnings("unchecked")
+    void preservesValidationModeThroughFactoryWrapperSerialization(boolean 
strict)
+            throws IOException, ClassNotFoundException {
+        TableTransformFactoryContext context =
+                configuration("BOOLEAN", strict ? true : null, false);
+        byte[] serialized =
+                SerializationUtils.serialize(
+                        new 
LLMTransformFactory().createTransform(context).createTransform());
+        try (ObjectInputStream input =
+                new ObjectInputStream(new ByteArrayInputStream(serialized)) {
+                    {
+                        enableResolveObject(true);
+                    }
+
+                    @Override
+                    protected Object resolveObject(Object value) {
+                        if (value instanceof LLMTransform) {
+                            // Retain the child for teardown; the factory 
wrapper has no close hook.
+                            transform = (LLMTransform) value;
+                        }
+                        return value;
+                    }
+                }) {
+            SeaTunnelMapTransform<SeaTunnelRow> restored =
+                    (SeaTunnelMapTransform<SeaTunnelRow>) input.readObject();
+            Assertions.assertNotNull(transform);
+            if (strict) {
+                Assertions.assertThrows(
+                        TransformException.class, () -> 
mapResponse("[\"unknown\"]", restored));
+            } else {
+                Assertions.assertEquals(false, mapResponse("[\"unknown\"]", 
restored).getField(1));
+            }
+            Assertions.assertEquals(true, mapResponse("[\"TrUe\"]", 
restored).getField(1));
+            Assertions.assertEquals(false, mapResponse("[\"FaLsE\"]", 
restored).getField(1));
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"[\"unknown\"]", "unknown"})
+    void validatesCustomArrayStringsAndPlainStrings(String content) throws 
IOException {
+        configure("BOOLEAN", true, true);
+        Assertions.assertThrows(TransformException.class, () -> 
mapResponse(content));
+    }
+
+    @Test
+    void acceptsCustomPlainBooleanString() throws IOException {
+        configure("BOOLEAN", true, true);
+        Assertions.assertEquals(true, mapResponse("TRUE").getField(1));
+    }
+
+    private void configure(String type, Boolean strict) {
+        configure(type, strict, false);
+    }
+
+    private void configure(String type, Boolean strict, boolean custom) {
+        TableTransformFactoryContext context = configuration(type, strict, 
custom);
+        transform = new LLMTransform(context.getOptions(), 
context.getCatalogTables().get(0));
+        transform.getProducedCatalogTable();
+        transform.open();
+    }
+
+    private TableTransformFactoryContext configuration(
+            String type, Boolean strict, boolean custom) {
+        Map<String, Object> options = new HashMap<>();
+        options.put("model_provider", custom ? "CUSTOM" : "OPENAI");
+        options.put("model", "test-model");
+        options.put("api_key", "test-key");
+        options.put("api_path", server.url("/chat/completions").toString());
+        options.put("prompt", "Classify the input");
+        options.put("output_data_type", type);
+        if (strict != null) {
+            options.put("strict_boolean_output", strict);
+        }
+        if (custom) {
+            Map<String, Object> customOptions = new HashMap<>();
+            customOptions.put(
+                    "custom_request_headers",
+                    Collections.singletonMap("Content-Type", 
"application/json"));
+            customOptions.put("custom_request_body", 
Collections.singletonMap("input", "${input}"));
+            customOptions.put("custom_response_parse", 
"$.choices[0].message.content");
+            options.put("custom_config", customOptions);
+        }
+        ReadonlyConfig config = ReadonlyConfig.fromMap(options);
+        ConfigValidator.of(config).validate(new 
LLMTransformFactory().optionRule());
+        CatalogTable table =
+                CatalogTable.of(
+                        TableIdentifier.of("catalog", TablePath.of("test", 
"input")),
+                        TableSchema.builder()
+                                .column(
+                                        PhysicalColumn.of(
+                                                "text",
+                                                BasicType.STRING_TYPE,
+                                                (Long) null,
+                                                true,
+                                                null,
+                                                null))
+                                .build(),
+                        Collections.emptyMap(),
+                        Collections.emptyList(),
+                        "test input");
+        return new TableTransformFactoryContext(
+                Collections.singletonList(table),
+                config,
+                Thread.currentThread().getContextClassLoader());
+    }
+
+    private SeaTunnelRow mapResponse(String content) throws IOException {
+        return mapResponse(content, transform);
+    }
+
+    private SeaTunnelRow mapResponse(String content, 
SeaTunnelMapTransform<SeaTunnelRow> target)
+            throws IOException {
+        ObjectNode response = MAPPER.createObjectNode();
+        
response.putArray("choices").addObject().putObject("message").put("content", 
content);
+        server.enqueue(new 
MockResponse().setBody(MAPPER.writeValueAsString(response)));
+        SeaTunnelRow input = new SeaTunnelRow(new Object[] {INPUT_VALUE});
+        input.setTableId("test.input");
+        input.setRowKind(RowKind.UPDATE_AFTER);
+        return target.map(input);
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/test/resources/llm/GenerateLegacyLLMFixture.java 
b/seatunnel-transforms-v2/src/test/resources/llm/GenerateLegacyLLMFixture.java
new file mode 100644
index 0000000000..00ca99dd23
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/resources/llm/GenerateLegacyLLMFixture.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import org.apache.seatunnel.api.table.catalog.TablePath;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.api.table.factory.TableTransformFactoryContext;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.common.utils.SerializationUtils;
+import org.apache.seatunnel.transform.nlpmodel.llm.LLMTransform;
+import org.apache.seatunnel.transform.nlpmodel.llm.LLMTransformFactory;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GenerateLegacyLLMFixture {
+    public static void main(String[] args) throws Exception {
+        // Fail closed if accidentally invoked with the new class instead of 
the legacy classes.
+        for (Field field : LLMTransform.class.getDeclaredFields()) {
+            if (field.getName().equals("strictBooleanOutput")
+                    || field.getName().equals("serialVersionUID")) {
+                throw new IllegalStateException("Compile and load the 
pre-change LLM classes first");
+            }
+        }
+        Map<String, Object> options = new HashMap<>();
+        options.put("model_provider", "OPENAI");
+        options.put("model", "test-model");
+        options.put("api_key", "test-key");
+        options.put("api_path", "http://127.0.0.1:1/legacy-fixture";);
+        options.put("prompt", "Classify the input");
+        options.put("output_data_type", "BOOLEAN");
+        CatalogTable table =
+                CatalogTable.of(
+                        TableIdentifier.of("catalog", TablePath.of("test", 
"input")),
+                        TableSchema.builder()
+                                .column(
+                                        PhysicalColumn.of(
+                                                "text",
+                                                BasicType.STRING_TYPE,
+                                                (Long) null,
+                                                true,
+                                                null,
+                                                null))
+                                .build(),
+                        Collections.emptyMap(),
+                        Collections.emptyList(),
+                        "test input");
+        TableTransformFactoryContext context =
+                new TableTransformFactoryContext(
+                        Collections.singletonList(table),
+                        ReadonlyConfig.fromMap(options),
+                        Thread.currentThread().getContextClassLoader());
+        byte[] bytes =
+                SerializationUtils.serialize(
+                        new 
LLMTransformFactory().createTransform(context).createTransform());
+        Files.write(Paths.get(args[0]), Base64.getMimeEncoder(76, new byte[] 
{'\n'}).encode(bytes));
+    }
+}
diff --git a/seatunnel-transforms-v2/src/test/resources/llm/README.md 
b/seatunnel-transforms-v2/src/test/resources/llm/README.md
new file mode 100644
index 0000000000..14a8f00e11
--- /dev/null
+++ b/seatunnel-transforms-v2/src/test/resources/llm/README.md
@@ -0,0 +1,56 @@
+<!--
+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.
+-->
+
+# Legacy LLM Serialization Fixture
+
+`legacy-boolean-wrapper.base64` contains Java serialization bytes from the
+factory-created `LLMMultiCatalogTransform` at commit
+`8bea8c681cacccbd99fa642ca0853718980b9234`, before `strict_boolean_output`.
+It contains an un-opened `LLMTransform`, with no explicit `serialVersionUID`
+and no `strictBooleanOutput` field. The configuration has only dummy values.
+No model request is made during generation.
+
+Set `JAVA_HOME` to JDK 8 or 11 and compile the transform module first. From the
+repository root, reproduce the implicit UID and compare the serialized bytes:
+
+```shell
+./mvnw -T1 -pl seatunnel-transforms-v2 -am -DskipTests verify
+bash seatunnel-transforms-v2/src/test/resources/llm/generate-legacy-fixture.sh
+```
+
+Expected `serialver` output:
+
+```text
+org.apache.seatunnel.transform.nlpmodel.llm.LLMTransform:    private static 
final long serialVersionUID = 4711686225005641485L;
+```
+
+The script compiles the four original LLM classes directly from that commit,
+ahead of the current module classes on the classpath. Their supporting classes
+were unchanged by this PR; local reactor-built API and Common classes take
+precedence over installed snapshots. The generator refuses the current LLM 
class shape.
+An optional output-file argument writes regenerated bytes instead of comparing.
+Do not regenerate the fixture using new production classes when changing the
+serialization contract: its purpose is to retain an independently produced old
+stream, not to make a failing compatibility test pass.
+
+`LLMBooleanOutputTest.readsLegacyFactoryWrapperWithValidationDisabled` reads
+these archived bytes with current classes, redirects only the dummy HTTP URL
+to a local server, and checks malformed-output coercion, boolean literals,
+and row metadata. Ordinary CI needs neither Git history nor a Java compiler
+for this test. A changed UID fails deserialization, and enabling strict mode
+for the old payload fails the malformed-output assertion. This is an old-plan
+serialization regression, not a full cluster-upgrade test.
diff --git 
a/seatunnel-transforms-v2/src/test/resources/llm/generate-legacy-fixture.sh 
b/seatunnel-transforms-v2/src/test/resources/llm/generate-legacy-fixture.sh
new file mode 100644
index 0000000000..5b083eb2f5
--- /dev/null
+++ b/seatunnel-transforms-v2/src/test/resources/llm/generate-legacy-fixture.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+# 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.
+
+set -euo pipefail
+: "${JAVA_HOME:?Set JAVA_HOME to JDK 8 or 11}"
+root=$(git rev-parse --show-toplevel)
+module="$root/seatunnel-transforms-v2"
+resource="$module/src/test/resources/llm"
+baseline=8bea8c681cacccbd99fa642ca0853718980b9234
+scratch=$(mktemp -d "$module/target/legacy-fixture.XXXXXX")
+trap 'rm -rf "$scratch"' EXIT
+mkdir -p "$scratch/classes"
+"$root/mvnw" -T1 -q -f "$module/pom.xml" dependency:build-classpath \
+    -Dmdep.outputFile="$scratch/classpath.txt"
+classpath="$module/target/classes:$root/seatunnel-api/target/classes:$root/seatunnel-common/target/classes:$(<"$scratch/classpath.txt")"
+for name in LLMTransform LLMTransformConfig LLMTransformFactory 
LLMMultiCatalogTransform; do
+    git -C "$root" show 
"$baseline:seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/$name.java"
 > "$scratch/$name.java"
+done
+"$JAVA_HOME/bin/javac" -source 8 -target 8 -cp "$classpath" \
+    -d "$scratch/classes" "$scratch/"*.java 
"$resource/GenerateLegacyLLMFixture.java"
+"$JAVA_HOME/bin/serialver" -classpath "$scratch/classes:$classpath" \
+    org.apache.seatunnel.transform.nlpmodel.llm.LLMTransform
+"$JAVA_HOME/bin/java" -cp "$scratch/classes:$classpath" 
GenerateLegacyLLMFixture \
+    "${1:-$scratch/legacy-boolean-wrapper.base64}"
+if [ "$#" -eq 0 ]; then
+    cmp "$scratch/legacy-boolean-wrapper.base64" 
"$resource/legacy-boolean-wrapper.base64"
+fi
diff --git 
a/seatunnel-transforms-v2/src/test/resources/llm/legacy-boolean-wrapper.base64 
b/seatunnel-transforms-v2/src/test/resources/llm/legacy-boolean-wrapper.base64
new file mode 100644
index 0000000000..8ffb6b1da1
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/resources/llm/legacy-boolean-wrapper.base64
@@ -0,0 +1,74 @@
+rO0ABXNyAERvcmcuYXBhY2hlLnNlYXR1bm5lbC50cmFuc2Zvcm0ubmxwbW9kZWwubGxtLkxMTU11
+bHRpQ2F0YWxvZ1RyYW5zZm9ybeJvyDh95WHhAgAAeHIARm9yZy5hcGFjaGUuc2VhdHVubmVsLnRy
+YW5zZm9ybS5jb21tb24uQWJzdHJhY3RNdWx0aUNhdGFsb2dNYXBUcmFuc2Zvcm2ljUyLnr2leQIA
+AHhyAENvcmcuYXBhY2hlLnNlYXR1bm5lbC50cmFuc2Zvcm0uY29tbW9uLkFic3RyYWN0TXVsdGlD
+YXRhbG9nVHJhbnNmb3JtYmWoJDXMnZECAANMABJpbnB1dENhdGFsb2dUYWJsZXN0ABBMamF2YS91
+dGlsL0xpc3Q7TAATb3V0cHV0Q2F0YWxvZ1RhYmxlc3EAfgADTAAMdHJhbnNmb3JtTWFwdAAPTGph
+dmEvdXRpbC9NYXA7eHBzcgAjamF2YS51dGlsLkNvbGxlY3Rpb25zJFNpbmdsZXRvbkxpc3Qq7ykQ
+PKeblwIAAUwAB2VsZW1lbnR0ABJMamF2YS9sYW5nL09iamVjdDt4cHNyADNvcmcuYXBhY2hlLnNl
+YXR1bm5lbC5hcGkudGFibGUuY2F0YWxvZy5DYXRhbG9nVGFibGUAAAAAAAAAAQIAB0wAC2NhdGFs
+b2dOYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAAHY29tbWVudHEAfgAKTAAIbWV0YWRhdGF0ADdM
+b3JnL2FwYWNoZS9zZWF0dW5uZWwvYXBpL3RhYmxlL2NhdGFsb2cvTWV0YWRhdGFTY2hlbWE7TAAH
+b3B0aW9uc3EAfgAETAANcGFydGl0aW9uS2V5c3EAfgADTAAHdGFibGVJZHQAOExvcmcvYXBhY2hl
+L3NlYXR1bm5lbC9hcGkvdGFibGUvY2F0YWxvZy9UYWJsZUlkZW50aWZpZXI7TAALdGFibGVTY2hl
+bWF0ADRMb3JnL2FwYWNoZS9zZWF0dW5uZWwvYXBpL3RhYmxlL2NhdGFsb2cvVGFibGVTY2hlbWE7
+eHB0AAdjYXRhbG9ndAAKdGVzdCBpbnB1dHNyADVvcmcuYXBhY2hlLnNlYXR1bm5lbC5hcGkudGFi
+bGUuY2F0YWxvZy5NZXRhZGF0YVNjaGVtYQAAAAAAAAABAgAAeHIANW9yZy5hcGFjaGUuc2VhdHVu
+bmVsLmFwaS50YWJsZS5jYXRhbG9nLkFic3RyYWN0U2NoZW1hAAAAAAAAAAECAAJMAAtjb2x1bW5O
+YW1lc3EAfgADTAAHY29sdW1uc3EAfgADeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2Gd
+AwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhzcQB+ABQAAAAAdwQAAAAAeHNyABFqYXZhLnV0aWwuSGFz
+aE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAA
+AAAAeHNxAH4AFAAAAAB3BAAAAAB4c3IANm9yZy5hcGFjaGUuc2VhdHVubmVsLmFwaS50YWJsZS5j
+YXRhbG9nLlRhYmxlSWRlbnRpZmllcgAAAAAAAAABAgAETAALY2F0YWxvZ05hbWVxAH4ACkwADGRh
+dGFiYXNlTmFtZXEAfgAKTAAKc2NoZW1hTmFtZXEAfgAKTAAJdGFibGVOYW1lcQB+AAp4cHEAfgAP
+dAAEdGVzdHB0AAVpbnB1dHNyADJvcmcuYXBhY2hlLnNlYXR1bm5lbC5hcGkudGFibGUuY2F0YWxv
+Zy5UYWJsZVNjaGVtYQAAAAAAAAABAgACTAAOY29uc3RyYWludEtleXNxAH4AA0wACnByaW1hcnlL
+ZXl0ADNMb3JnL2FwYWNoZS9zZWF0dW5uZWwvYXBpL3RhYmxlL2NhdGFsb2cvUHJpbWFyeUtleTt4
+cQB+ABJzcQB+ABQAAAABdwQAAAABdAAEdGV4dHhzcQB+ABQAAAABdwQAAAABc3IANW9yZy5hcGFj
+aGUuc2VhdHVubmVsLmFwaS50YWJsZS5jYXRhbG9nLlBoeXNpY2FsQ29sdW1uAAAAAAAAAAECAAB4
+cgAtb3JnLmFwYWNoZS5zZWF0dW5uZWwuYXBpLnRhYmxlLmNhdGFsb2cuQ29sdW1u//////////8C
+AA5aAAppc1Vuc2lnbmVkWgAKaXNaZXJvRmlsbFoACG51bGxhYmxlTAAGYml0TGVudAAQTGphdmEv
+bGFuZy9Mb25nO0wADGNvbHVtbkxlbmd0aHEAfgAmTAAHY29tbWVudHEAfgAKTAAIZGF0YVR5cGV0
+ADdMb3JnL2FwYWNoZS9zZWF0dW5uZWwvYXBpL3RhYmxlL3R5cGUvU2VhVHVubmVsRGF0YVR5cGU7
+TAAMZGVmYXVsdFZhbHVlcQB+AAdMABBsb25nQ29sdW1uTGVuZ3RocQB+ACZMAARuYW1lcQB+AApM
+AAdvcHRpb25zcQB+AARMAAVzY2FsZXQAE0xqYXZhL2xhbmcvSW50ZWdlcjtMAAhzaW5rVHlwZXEA
+fgAKTAAKc291cmNlVHlwZXEAfgAKeHAAAAFzcgAOamF2YS5sYW5nLkxvbmc7i+SQzI8j3wIAAUoA
+BXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAAAAAABwcHNyAC1vcmcu
+YXBhY2hlLnNlYXR1bm5lbC5hcGkudGFibGUudHlwZS5CYXNpY1R5cGUAAAAAAAAAAgIAAkwAB3Nx
+bFR5cGV0AC1Mb3JnL2FwYWNoZS9zZWF0dW5uZWwvYXBpL3RhYmxlL3R5cGUvU3FsVHlwZTtMAAl0
+eXBlQ2xhc3N0ABFMamF2YS9sYW5nL0NsYXNzO3hwfnIAK29yZy5hcGFjaGUuc2VhdHVubmVsLmFw
+aS50YWJsZS50eXBlLlNxbFR5cGUAAAAAAAAAABIAAHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAA
+EgAAeHB0AAZTVFJJTkd2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHBwcHEAfgAicHBw
+cHhzcQB+ABQAAAAAdwQAAAAAeHBzcQB+ABQAAAABdwQAAAABc3EAfgAJcQB+AA9xAH4AEHEAfgAT
+c3EAfgAXP0AAAAAAAAB3CAAAABAAAAAAeHNxAH4AFAAAAAB3BAAAAAB4c3EAfgAacQB+AA9xAH4A
+HHBxAH4AHXNxAH4AHnNxAH4AFAAAAAJ3BAAAAAJxAH4AInQACmxsbV9vdXRwdXR4c3EAfgAUAAAA
+AncEAAAAAnNxAH4AJAAAAXEAfgAscHBxAH4AMHBwcQB+ACJwcHBwc3EAfgAkAAABcQB+ACxwdAAU
+T3V0cHV0IGNvbHVtbiBvZiBMTE1zcQB+AC1+cQB+ADF0AAdCT09MRUFOdnIAEWphdmEubGFuZy5C
+b29sZWFuzSBygNWc+u4CAAFaAAV2YWx1ZXhwcHBxAH4AP3BwcHB4c3EAfgAUAAAAAHcEAAAAAHhw
+eHNxAH4AFz9AAAAAAAAMdwgAAAAQAAAAAXQACnRlc3QuaW5wdXRzcgA4b3JnLmFwYWNoZS5zZWF0
+dW5uZWwudHJhbnNmb3JtLm5scG1vZGVsLmxsbS5MTE1UcmFuc2Zvcm1BY0Wodo//DQIAA0wABmNv
+bmZpZ3QAN0xvcmcvYXBhY2hlL3NlYXR1bm5lbC9hcGkvY29uZmlndXJhdGlvbi9SZWFkb25seUNv
+bmZpZztMAAVtb2RlbHQAOkxvcmcvYXBhY2hlL3NlYXR1bm5lbC90cmFuc2Zvcm0vbmxwbW9kZWwv
+bGxtL3JlbW90ZS9Nb2RlbDtMAA5vdXRwdXREYXRhVHlwZXEAfgAneHIAQG9yZy5hcGFjaGUuc2Vh
+dHVubmVsLnRyYW5zZm9ybS5jb21tb24uU2luZ2xlRmllbGRPdXRwdXRUcmFuc2Zvcm2jHvtYcP9U
+QAIAAkkACmZpZWxkSW5kZXhMABVyb3dDb250YWluZXJHZW5lcmF0b3J0AEZMb3JnL2FwYWNoZS9z
+ZWF0dW5uZWwvdHJhbnNmb3JtL2NvbW1vbi9TZWFUdW5uZWxSb3dDb250YWluZXJHZW5lcmF0b3I7
+eHIASG9yZy5hcGFjaGUuc2VhdHVubmVsLnRyYW5zZm9ybS5jb21tb24uQWJzdHJhY3RDYXRhbG9n
+U3VwcG9ydE1hcFRyYW5zZm9ybWxQVbZiwsG7AgAAeHIAQG9yZy5hcGFjaGUuc2VhdHVubmVsLnRy
+YW5zZm9ybS5jb21tb24uQWJzdHJhY3RTZWFUdW5uZWxUcmFuc2Zvcm1wj/I+nfMk5gIAA0wAEWlu
+cHV0Q2F0YWxvZ1RhYmxldAA1TG9yZy9hcGFjaGUvc2VhdHVubmVsL2FwaS90YWJsZS9jYXRhbG9n
+L0NhdGFsb2dUYWJsZTtMABJvdXRwdXRDYXRhbG9nVGFibGVxAH4AU0wAEXJvd0Vycm9ySGFuZGxl
+V2F5dAA2TG9yZy9hcGFjaGUvc2VhdHVubmVsL3RyYW5zZm9ybS9jb21tb24vRXJyb3JIYW5kbGVX
+YXk7eHBxAH4ADnEAfgA5fnIANG9yZy5hcGFjaGUuc2VhdHVubmVsLnRyYW5zZm9ybS5jb21tb24u
+RXJyb3JIYW5kbGVXYXkAAAAAAAAAABIAAHhxAH4AMnQABEZBSUwAAAABc3IAQm9yZy5hcGFjaGUu
+c2VhdHVubmVsLnRyYW5zZm9ybS5jb21tb24uU2luZ2xlRmllbGRPdXRwdXRUcmFuc2Zvcm0kMW60
+3vythD6tAgADSQAUdmFsJGlucHV0RmllbGRMZW5ndGhJABV2YWwkb3V0cHV0RmllbGRMZW5ndGhM
+AAZ0aGlzJDB0AEJMb3JnL2FwYWNoZS9zZWF0dW5uZWwvdHJhbnNmb3JtL2NvbW1vbi9TaW5nbGVG
+aWVsZE91dHB1dFRyYW5zZm9ybTt4cgBEb3JnLmFwYWNoZS5zZWF0dW5uZWwudHJhbnNmb3JtLmNv
+bW1vbi5TZWFUdW5uZWxSb3dDb250YWluZXJHZW5lcmF0b3JmXkon0O1X6gIAAHhwAAAAAQAAAAJx
+AH4AVXNyADVvcmcuYXBhY2hlLnNlYXR1bm5lbC5hcGkuY29uZmlndXJhdGlvbi5SZWFkb25seUNv
+bmZpZwAAAAAAAAABAgABTAAIY29uZkRhdGFxAH4ABHhwc3EAfgAXP0AAAAAAAAx3CAAAABAAAAAG
+dAAQb3V0cHV0X2RhdGFfdHlwZXEAfgBGdAAObW9kZWxfcHJvdmlkZXJ0AAZPUEVOQUl0AAdhcGlf
+a2V5dAAIdGVzdC1rZXl0AAVtb2RlbHQACnRlc3QtbW9kZWx0AAhhcGlfcGF0aHQAIWh0dHA6Ly8x
+MjcuMC4wLjE6MS9sZWdhY3ktZml4dHVyZXQABnByb21wdHQAEkNsYXNzaWZ5IHRoZSBpbnB1dHhw
+cQB+AER4
\ No newline at end of file

Reply via email to