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

yuxiqian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git


The following commit(s) were added to refs/heads/master by this push:
     new e3f0caf44 [FLINK-39568][runtime] Introduce AI model client function 
API (#4504)
e3f0caf44 is described below

commit e3f0caf4492756016a36a0c8c302898da458d2b1
Author: haruki <[email protected]>
AuthorDate: Thu Aug 13 21:45:54 2026 +0800

    [FLINK-39568][runtime] Introduce AI model client function API (#4504)
---
 .../cli/parser/YamlPipelineDefinitionParser.java   |  96 ++++++++--
 .../parser/YamlPipelineDefinitionParserTest.java   | 109 ++++++++++++
 .../flink/cdc/common/model/AiModelClient.java      |  42 +++++
 .../cdc/common/model/AiModelClientFactory.java     |  35 ++++
 .../flink/cdc/common/model/ModelContext.java       |  36 ++++
 .../common/model/abilities/SupportsEmbedding.java  |  32 ++++
 .../model/abilities/SupportsTextGeneration.java    |  35 ++++
 flink-cdc-composer/pom.xml                         |   8 +-
 .../flink/cdc/composer/definition/ModelDef.java    |  99 ++++++++---
 .../flink/translator/TransformTranslator.java      |  85 ++++++++-
 .../flink/FlinkPipelineAiFunctionITCase.java       | 197 +++++++++++++++++++++
 .../flink-cdc-pipeline-e2e-tests/pom.xml           |  10 ++
 .../cdc/pipeline/tests/AiFunctionE2eITCase.java    | 123 +++++++++++++
 .../src/test/resources/rules/malformed.yaml        |  17 --
 .../{ => flink-cdc-pipeline-model-dummy}/pom.xml   |  42 +----
 .../flink/cdc/models/dummy/DummyModelClient.java   |  61 +++++++
 .../cdc/models/dummy/DummyModelClientFactory.java  |  56 ++++++
 .../org.apache.flink.cdc.common.factories.Factory  |  16 ++
 .../models/dummy/DummyModelClientFactoryTest.java  |  97 ++++++++++
 .../{ => flink-cdc-pipeline-model-legacy}/pom.xml  |   7 +-
 .../flink/cdc/runtime/model/ModelOptions.java      |   0
 .../flink/cdc/runtime/model/OpenAIChatModel.java   |   0
 .../cdc/runtime/model/OpenAIEmbeddingModel.java    |   0
 .../cdc/runtime/model/TestOpenAIChatModel.java     |   0
 .../runtime/model/TestOpenAIEmbeddingModel.java    |   0
 flink-cdc-pipeline-model/pom.xml                   |  65 +------
 .../cdc/runtime/ai/AiEmbeddingFunctionDef.java     |  50 ++++++
 .../flink/cdc/runtime/ai/AiTextFunctionDef.java    |  61 +++++++
 .../cdc/runtime/functions/impl/AiFunctions.java    |  85 +++++++++
 .../operators/transform/PostTransformOperator.java |  47 ++++-
 .../transform/PostTransformOperatorBuilder.java    |  10 +-
 .../transform/ProjectionColumnProcessor.java       |  33 +++-
 .../transform/TransformExpressionCompiler.java     |  22 +++
 .../transform/TransformFilterProcessor.java        |  19 +-
 .../transform/TransformProjectionProcessor.java    |   9 +-
 .../flink/cdc/runtime/parser/JaninoCompiler.java   |  33 +++-
 .../flink/cdc/runtime/parser/TransformParser.java  |  56 +++++-
 .../metadata/AiFunctionSqlOperatorTable.java       | 110 ++++++++++++
 .../runtime/functions/impl/AiFunctionsTest.java    |  91 ++++++++++
 .../cdc/runtime/parser/AiFunctionParserTest.java   | 111 ++++++++++++
 40 files changed, 1833 insertions(+), 172 deletions(-)

diff --git 
a/flink-cdc-cli/src/main/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParser.java
 
b/flink-cdc-cli/src/main/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParser.java
index 831bb5e65..b5519afd5 100644
--- 
a/flink-cdc-cli/src/main/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParser.java
+++ 
b/flink-cdc-cli/src/main/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParser.java
@@ -46,6 +46,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -96,9 +97,11 @@ public class YamlPipelineDefinitionParser implements 
PipelineDefinitionParser {
     private static final String UDF_OPTIONS_KEY = "options";
 
     // Model related keys
-    private static final String MODEL_NAME_KEY = "model-name";
-
-    private static final String MODEL_CLASS_NAME_KEY = "class-name";
+    private static final String MODEL_NAME_KEY = "name";
+    private static final String MODEL_TYPE_KEY = "type";
+    private static final String MODEL_OPTIONS_KEY = "options";
+    private static final String LEGACY_MODEL_NAME_KEY = "model-name";
+    private static final String LEGACY_MODEL_CLASS_NAME_KEY = "class-name";
 
     public static final String TRANSFORM_PRIMARY_KEY_KEY = "primary-keys";
 
@@ -145,7 +148,6 @@ public class YamlPipelineDefinitionParser implements 
PipelineDefinitionParser {
 
             Optional.ofNullable(
                             ((ObjectNode) 
pipelineDefJsonNode.get(PIPELINE_KEY)).remove(MODEL_KEY))
-                    .map(node -> validateArray("model", node))
                     .ifPresent(node -> modelDefs.addAll(parseModels(node)));
         }
 
@@ -428,24 +430,98 @@ public class YamlPipelineDefinitionParser implements 
PipelineDefinitionParser {
         } else {
             modelDefs.add(convertJsonNodeToModelDef(modelsNode));
         }
+        Set<String> seenNames = new HashSet<>();
+        for (ModelDef model : modelDefs) {
+            if (!seenNames.add(model.getName())) {
+                throw new IllegalArgumentException(
+                        "Duplicate model name '" + model.getName() + "' in 
pipeline definition.");
+            }
+        }
         return modelDefs;
     }
 
     private ModelDef convertJsonNodeToModelDef(JsonNode modelNode) {
+        Preconditions.checkArgument(
+                modelNode instanceof ObjectNode,
+                "`model` in `pipeline` should be an object, but got %s",
+                modelNode);
+        ObjectNode node = ((ObjectNode) modelNode).deepCopy();
+        boolean usesNewFormat = node.has(MODEL_NAME_KEY) || 
node.has(MODEL_TYPE_KEY);
+        boolean usesLegacyFormat =
+                node.has(LEGACY_MODEL_NAME_KEY) && 
node.has(LEGACY_MODEL_CLASS_NAME_KEY);
+        Preconditions.checkArgument(
+                !(usesNewFormat && usesLegacyFormat),
+                "Model definition must use either name/type/options or 
model-name/class-name, but not both: %s",
+                modelNode);
+
+        if (usesLegacyFormat) {
+            String modelName =
+                    checkNotNull(
+                                    node.get(LEGACY_MODEL_NAME_KEY),
+                                    "Missing required field \"%s\" in `model`",
+                                    LEGACY_MODEL_NAME_KEY)
+                            .asText();
+            validateModelName(modelName);
+            String className =
+                    checkNotNull(
+                                    node.get(LEGACY_MODEL_CLASS_NAME_KEY),
+                                    "Missing required field \"%s\" in `model`",
+                                    LEGACY_MODEL_CLASS_NAME_KEY)
+                            .asText();
+            Map<String, String> parameters =
+                    mapper.convertValue(node, new TypeReference<Map<String, 
String>>() {});
+            return new ModelDef(modelName, className, parameters);
+        }
+
         String name =
                 checkNotNull(
-                                modelNode.get(MODEL_NAME_KEY),
+                                node.remove(MODEL_NAME_KEY),
                                 "Missing required field \"%s\" in `model`",
                                 MODEL_NAME_KEY)
                         .asText();
-        String model =
+        validateModelName(name);
+        String type =
                 checkNotNull(
-                                modelNode.get(MODEL_CLASS_NAME_KEY),
+                                node.remove(MODEL_TYPE_KEY),
                                 "Missing required field \"%s\" in `model`",
-                                MODEL_CLASS_NAME_KEY)
+                                MODEL_TYPE_KEY)
                         .asText();
-        Map<String, String> properties = mapper.convertValue(modelNode, 
Map.class);
-        return new ModelDef(name, model, properties);
+        Preconditions.checkArgument(
+                !StringUtils.isNullOrWhitespaceOnly(type),
+                "Model type must not be empty for model '%s'.",
+                name);
+
+        Map<String, String> options = new LinkedHashMap<>();
+        JsonNode optionsNode = node.remove(MODEL_OPTIONS_KEY);
+        if (optionsNode != null) {
+            Preconditions.checkArgument(
+                    optionsNode instanceof ObjectNode,
+                    "Model options must be an object, but got %s",
+                    optionsNode);
+            options.putAll(
+                    mapper.convertValue(optionsNode, new 
TypeReference<Map<String, String>>() {}));
+        }
+        node.fields()
+                .forEachRemaining(
+                        entry -> {
+                            Preconditions.checkArgument(
+                                    !options.containsKey(entry.getKey()),
+                                    "Duplicate model option '%s' for model 
'%s'.",
+                                    entry.getKey(),
+                                    name);
+                            options.put(entry.getKey(), 
entry.getValue().asText());
+                        });
+        return ModelDef.of(name, type, options);
+    }
+
+    private void validateModelName(String name) {
+        Preconditions.checkArgument(
+                name.matches("[a-zA-Z_][a-zA-Z0-9_]*") && 
!name.startsWith("__"),
+                "Model name \"%s\" is not a valid identifier. "
+                        + "It must start with a letter or underscore, "
+                        + "contain only letters, digits, or underscores, "
+                        + "and must not start with double underscores.",
+                name);
     }
 
     private void validateJsonNodeKeys(
diff --git 
a/flink-cdc-cli/src/test/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParserTest.java
 
b/flink-cdc-cli/src/test/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParserTest.java
index 4ecc72d14..834831b6c 100644
--- 
a/flink-cdc-cli/src/test/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParserTest.java
+++ 
b/flink-cdc-cli/src/test/java/org/apache/flink/cdc/cli/parser/YamlPipelineDefinitionParserTest.java
@@ -364,6 +364,115 @@ class YamlPipelineDefinitionParserTest {
                                                 .build())));
     }
 
+    @Test
+    void testParsingFactoryBasedModel() throws Exception {
+        String yaml =
+                "source:\n"
+                        + "  type: values\n"
+                        + "sink:\n"
+                        + "  type: values\n"
+                        + "pipeline:\n"
+                        + "  model:\n"
+                        + "    name: completion_model\n"
+                        + "    type: dummy\n"
+                        + "    options:\n"
+                        + "      debug: true\n"
+                        + "      retries: 3\n";
+
+        PipelineDef pipelineDef =
+                new YamlPipelineDefinitionParser().parse(yaml, new 
Configuration());
+
+        assertThat(pipelineDef.getModels())
+                .containsExactly(
+                        ModelDef.of(
+                                "completion_model",
+                                "dummy",
+                                ImmutableMap.of("debug", "true", "retries", 
"3")));
+    }
+
+    @Test
+    void testParsingFactoryBasedModelWithFlatModelNameOption() throws 
Exception {
+        String yaml =
+                "source:\n"
+                        + "  type: values\n"
+                        + "sink:\n"
+                        + "  type: values\n"
+                        + "pipeline:\n"
+                        + "  model:\n"
+                        + "    name: completion_model\n"
+                        + "    type: openai-compatible\n"
+                        + "    model-name: model-v1\n";
+
+        PipelineDef pipelineDef =
+                new YamlPipelineDefinitionParser().parse(yaml, new 
Configuration());
+
+        assertThat(pipelineDef.getModels())
+                .containsExactly(
+                        ModelDef.of(
+                                "completion_model",
+                                "openai-compatible",
+                                Collections.singletonMap("model-name", 
"model-v1")));
+    }
+
+    @Test
+    void testParsingLegacyAndFactoryBasedModelsTogether() throws Exception {
+        String yaml =
+                "source:\n"
+                        + "  type: values\n"
+                        + "sink:\n"
+                        + "  type: values\n"
+                        + "pipeline:\n"
+                        + "  model:\n"
+                        + "    - model-name: LEGACY_CHAT\n"
+                        + "      class-name: OpenAIChatModel\n"
+                        + "      openai.model: legacy-model\n"
+                        + "    - name: completion_model\n"
+                        + "      type: dummy\n"
+                        + "      debug: false\n";
+
+        PipelineDef pipelineDef =
+                new YamlPipelineDefinitionParser().parse(yaml, new 
Configuration());
+
+        assertThat(pipelineDef.getModels()).hasSize(2);
+        assertThat(pipelineDef.getModels().get(0).isLegacy()).isTrue();
+        assertThat(pipelineDef.getModels().get(1))
+                .isEqualTo(
+                        ModelDef.of(
+                                "completion_model",
+                                "dummy",
+                                Collections.singletonMap("debug", "false")));
+    }
+
+    @Test
+    void testDuplicateAndInvalidModelNames() {
+        String duplicate =
+                "source:\n"
+                        + "  type: values\n"
+                        + "sink:\n"
+                        + "  type: values\n"
+                        + "pipeline:\n"
+                        + "  model:\n"
+                        + "    - name: duplicated\n"
+                        + "      type: dummy\n"
+                        + "    - model-name: duplicated\n"
+                        + "      class-name: OpenAIChatModel\n";
+        String invalid =
+                "source:\n"
+                        + "  type: values\n"
+                        + "sink:\n"
+                        + "  type: values\n"
+                        + "pipeline:\n"
+                        + "  model:\n"
+                        + "    name: invalid-name\n"
+                        + "    type: dummy\n";
+
+        YamlPipelineDefinitionParser parser = new 
YamlPipelineDefinitionParser();
+        assertThatThrownBy(() -> parser.parse(duplicate, new Configuration()))
+                .hasMessage("Duplicate model name 'duplicated' in pipeline 
definition.");
+        assertThatThrownBy(() -> parser.parse(invalid, new Configuration()))
+                .hasMessageContaining("is not a valid identifier");
+    }
+
     private final PipelineDef fullDef =
             new PipelineDef(
                     new SourceDef(
diff --git 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/AiModelClient.java
 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/AiModelClient.java
new file mode 100644
index 000000000..0e46508bb
--- /dev/null
+++ 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/AiModelClient.java
@@ -0,0 +1,42 @@
+/*
+ * 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.flink.cdc.common.model;
+
+import org.apache.flink.cdc.common.annotation.Experimental;
+
+import java.io.Serializable;
+
+/**
+ * Marker interface for a runtime AI model client. Concrete capabilities are 
declared via ability
+ * interfaces in {@code org.apache.flink.cdc.common.model.abilities}.
+ *
+ * <p>Implementations must be {@link Serializable} so that they can be 
distributed across Flink task
+ * managers together with the operator that holds them.
+ */
+@Experimental
+public interface AiModelClient extends Serializable, AutoCloseable {
+
+    default void open() throws Exception {
+        // Do nothing
+    }
+
+    @Override
+    default void close() throws Exception {
+        // Do nothing
+    }
+}
diff --git 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/AiModelClientFactory.java
 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/AiModelClientFactory.java
new file mode 100644
index 000000000..3f50c5295
--- /dev/null
+++ 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/AiModelClientFactory.java
@@ -0,0 +1,35 @@
+/*
+ * 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.flink.cdc.common.model;
+
+import org.apache.flink.cdc.common.annotation.Experimental;
+import org.apache.flink.cdc.common.factories.Factory;
+
+/**
+ * A {@link Factory} that creates {@link AiModelClient} instances. The factory 
identifier maps to
+ * the {@code type} field of a pipeline model definition.
+ */
+@Experimental
+public interface AiModelClientFactory extends Factory {
+
+    /**
+     * Creates a new {@link AiModelClient} from the given context. The 
returned client is serialized
+     * with the transform operator and opened on task managers.
+     */
+    AiModelClient createClient(ModelContext context);
+}
diff --git 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/ModelContext.java
 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/ModelContext.java
new file mode 100644
index 000000000..a6b30250a
--- /dev/null
+++ 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/ModelContext.java
@@ -0,0 +1,36 @@
+/*
+ * 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.flink.cdc.common.model;
+
+import org.apache.flink.cdc.common.annotation.Experimental;
+
+import java.util.Map;
+
+/** Context passed to {@link AiModelClientFactory#createClient} at pipeline 
assembly time. */
+@Experimental
+public interface ModelContext {
+
+    /** The logical name of this model as declared in the pipeline YAML. */
+    String getModelName();
+
+    /** Raw key/value options from the pipeline YAML {@code model.options} 
block. */
+    Map<String, String> getOptions();
+
+    /** Class loader to use when loading implementation classes. */
+    ClassLoader getClassLoader();
+}
diff --git 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/abilities/SupportsEmbedding.java
 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/abilities/SupportsEmbedding.java
new file mode 100644
index 000000000..d6c8fc7d1
--- /dev/null
+++ 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/abilities/SupportsEmbedding.java
@@ -0,0 +1,32 @@
+/*
+ * 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.flink.cdc.common.model.abilities;
+
+import org.apache.flink.cdc.common.annotation.Experimental;
+import org.apache.flink.cdc.common.model.AiModelClient;
+
+/**
+ * Ability interface for {@link AiModelClient} implementations that can 
produce dense vector
+ * embeddings from text input.
+ */
+@Experimental
+public interface SupportsEmbedding {
+
+    /** Converts the given text into a dense float vector. */
+    float[] embed(String text);
+}
diff --git 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/abilities/SupportsTextGeneration.java
 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/abilities/SupportsTextGeneration.java
new file mode 100644
index 000000000..eb1ea7362
--- /dev/null
+++ 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/model/abilities/SupportsTextGeneration.java
@@ -0,0 +1,35 @@
+/*
+ * 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.flink.cdc.common.model.abilities;
+
+import org.apache.flink.cdc.common.annotation.Experimental;
+import org.apache.flink.cdc.common.model.AiModelClient;
+
+/**
+ * Ability interface for {@link AiModelClient} implementations that can 
perform chat-style text
+ * generation given a system prompt and a user input.
+ */
+@Experimental
+public interface SupportsTextGeneration {
+
+    /**
+     * Generates text based on a system-level prompt and a user-provided input 
message. Returns a
+     * JSON string conforming to the output schema declared by the calling AI 
function.
+     */
+    String generate(String systemPrompt, String userInput);
+}
diff --git a/flink-cdc-composer/pom.xml b/flink-cdc-composer/pom.xml
index 4161491ba..693d93e1d 100644
--- a/flink-cdc-composer/pom.xml
+++ b/flink-cdc-composer/pom.xml
@@ -86,6 +86,12 @@ limitations under the License.
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.flink</groupId>
+            <artifactId>flink-cdc-pipeline-model-dummy</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
 
         <!-- This is for testing Scala UDF.-->
         <dependency>
@@ -139,4 +145,4 @@ limitations under the License.
         </profile>
     </profiles>
 
-</project>
\ No newline at end of file
+</project>
diff --git 
a/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/definition/ModelDef.java
 
b/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/definition/ModelDef.java
index 21cc6befa..1439e329a 100644
--- 
a/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/definition/ModelDef.java
+++ 
b/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/definition/ModelDef.java
@@ -17,44 +17,89 @@
 
 package org.apache.flink.cdc.composer.definition;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 
-/**
- * Common properties of model.
- *
- * <p>A transformation definition contains:
- *
- * <ul>
- *   <li>modelName: The name of function.
- *   <li>className: The model to transform data.
- *   <li>parameters: The parameters that used to configure the model.
- * </ul>
- */
+/** Definition of an AI model declared in a pipeline. */
 public class ModelDef {
 
-    private final String modelName;
+    private final String name;
+
+    private final String type;
 
     private final String className;
 
-    private final Map<String, String> parameters;
+    private final Map<String, String> options;
+
+    private final boolean legacy;
 
+    /**
+     * Creates a legacy model definition backed by a model UDF class.
+     *
+     * @deprecated Use {@link #of(String, String, Map)} for factory-based AI 
model clients.
+     */
+    @Deprecated
     public ModelDef(String modelName, String className, Map<String, String> 
parameters) {
-        this.modelName = modelName;
+        this(modelName, null, className, parameters, true);
+    }
+
+    private ModelDef(
+            String name,
+            String type,
+            String className,
+            Map<String, String> options,
+            boolean legacy) {
+        this.name = name;
+        this.type = type;
         this.className = className;
-        this.parameters = parameters;
+        this.options = options == null ? Collections.emptyMap() : options;
+        this.legacy = legacy;
+    }
+
+    /** Creates a factory-based AI model client definition. */
+    public static ModelDef of(String name, String type, Map<String, String> 
options) {
+        return new ModelDef(name, type, null, options, false);
     }
 
+    public String getName() {
+        return name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public Map<String, String> getOptions() {
+        return options;
+    }
+
+    public boolean isLegacy() {
+        return legacy;
+    }
+
+    /**
+     * @deprecated Use {@link #getName()}.
+     */
+    @Deprecated
     public String getModelName() {
-        return modelName;
+        return name;
     }
 
+    /**
+     * @deprecated Factory-based models use {@link #getType()}.
+     */
+    @Deprecated
     public String getClassName() {
         return className;
     }
 
+    /**
+     * @deprecated Use {@link #getOptions()}.
+     */
+    @Deprecated
     public Map<String, String> getParameters() {
-        return parameters;
+        return options;
     }
 
     @Override
@@ -66,27 +111,29 @@ public class ModelDef {
             return false;
         }
         ModelDef modelDef = (ModelDef) o;
-        return Objects.equals(modelName, modelDef.modelName)
+        return Objects.equals(name, modelDef.name)
+                && Objects.equals(type, modelDef.type)
                 && Objects.equals(className, modelDef.className)
-                && Objects.equals(parameters, modelDef.parameters);
+                && Objects.equals(options, modelDef.options)
+                && legacy == modelDef.legacy;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(modelName, className, parameters);
+        return Objects.hash(name, type, className, options, legacy);
     }
 
     @Override
     public String toString() {
         return "ModelDef{"
                 + "name='"
-                + modelName
-                + '\''
-                + ", model='"
-                + className
+                + name
                 + '\''
-                + ", parameters="
-                + parameters
+                + (legacy ? ", className='" + className + '\'' : ", type='" + 
type + '\'')
+                + ", options="
+                + options
+                + ", legacy="
+                + legacy
                 + '}';
     }
 }
diff --git 
a/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/flink/translator/TransformTranslator.java
 
b/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/flink/translator/TransformTranslator.java
index 6e296db4d..0ccb3afd8 100644
--- 
a/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/flink/translator/TransformTranslator.java
+++ 
b/flink-cdc-composer/src/main/java/org/apache/flink/cdc/composer/flink/translator/TransformTranslator.java
@@ -18,20 +18,30 @@
 package org.apache.flink.cdc.composer.flink.translator;
 
 import org.apache.flink.api.java.tuple.Tuple3;
+import org.apache.flink.cdc.common.configuration.Configuration;
 import org.apache.flink.cdc.common.event.Event;
+import org.apache.flink.cdc.common.factories.FactoryHelper;
+import org.apache.flink.cdc.common.model.AiModelClient;
+import org.apache.flink.cdc.common.model.AiModelClientFactory;
+import org.apache.flink.cdc.common.model.ModelContext;
 import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
 import org.apache.flink.cdc.composer.definition.ModelDef;
 import org.apache.flink.cdc.composer.definition.TransformDef;
 import org.apache.flink.cdc.composer.definition.UdfDef;
+import org.apache.flink.cdc.composer.utils.FactoryDiscoveryUtils;
 import org.apache.flink.cdc.runtime.operators.transform.PostTransformOperator;
 import 
org.apache.flink.cdc.runtime.operators.transform.PostTransformOperatorBuilder;
 import org.apache.flink.cdc.runtime.operators.transform.PreTransformOperator;
 import 
org.apache.flink.cdc.runtime.operators.transform.PreTransformOperatorBuilder;
+import org.apache.flink.cdc.runtime.parser.TransformParser;
 import org.apache.flink.cdc.runtime.typeutils.EventTypeInfo;
 import org.apache.flink.streaming.api.datastream.DataStream;
 
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -53,6 +63,7 @@ public class TransformTranslator {
         if (transforms.isEmpty()) {
             return input;
         }
+        validateModelReferences(transforms, models);
         return input.transform(
                 "Transform:Schema",
                 new EventTypeInfo(),
@@ -85,7 +96,10 @@ public class TransformTranslator {
                                 .map(this::udfDefToUDFTuple)
                                 .collect(Collectors.toList()))
                 .addUdfFunctions(
-                        
models.stream().map(this::modelToUDFTuple).collect(Collectors.toList()));
+                        models.stream()
+                                .filter(ModelDef::isLegacy)
+                                .map(this::modelToUDFTuple)
+                                .collect(Collectors.toList()));
 
         return preTransformFunctionBuilder.build();
     }
@@ -120,7 +134,11 @@ public class TransformTranslator {
         postTransformFunctionBuilder.addUdfFunctions(
                 
udfFunctions.stream().map(this::udfDefToUDFTuple).collect(Collectors.toList()));
         postTransformFunctionBuilder.addUdfFunctions(
-                
models.stream().map(this::modelToUDFTuple).collect(Collectors.toList()));
+                models.stream()
+                        .filter(ModelDef::isLegacy)
+                        .map(this::modelToUDFTuple)
+                        .collect(Collectors.toList()));
+        postTransformFunctionBuilder.addModelClients(loadModelClients(models));
         return input.transform(
                         "Transform:Data", new EventTypeInfo(), 
postTransformFunctionBuilder.build())
                 .uid(operatorUidGenerator.generateUid("post-transform"));
@@ -133,7 +151,70 @@ public class TransformTranslator {
                 model.getParameters());
     }
 
+    private Map<String, AiModelClient> loadModelClients(List<ModelDef> models) 
{
+        List<ModelDef> clientModels =
+                models.stream().filter(model -> 
!model.isLegacy()).collect(Collectors.toList());
+        if (clientModels.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+        Map<String, AiModelClient> clients = new LinkedHashMap<>();
+        for (ModelDef model : clientModels) {
+            AiModelClientFactory factory =
+                    FactoryDiscoveryUtils.getFactoryByIdentifier(
+                            model.getType(), AiModelClientFactory.class);
+            FactoryHelper.createFactoryHelper(
+                            factory,
+                            new FactoryHelper.DefaultContext(
+                                    Configuration.fromMap(model.getOptions()),
+                                    new Configuration(),
+                                    classLoader))
+                    .validate();
+            ModelContext context = new DefaultModelContext(model, classLoader);
+            clients.put(model.getName(), factory.createClient(context));
+        }
+        return clients;
+    }
+
+    private void validateModelReferences(List<TransformDef> transforms, 
List<ModelDef> models) {
+        Set<String> clientModelNames =
+                models.stream()
+                        .filter(model -> !model.isLegacy())
+                        .map(ModelDef::getName)
+                        .collect(Collectors.toSet());
+        for (TransformDef transform : transforms) {
+            TransformParser.validateAiModelReferences(
+                    transform.getProjection(), transform.getFilter(), 
clientModelNames);
+        }
+    }
+
     private Tuple3<String, String, Map<String, String>> 
udfDefToUDFTuple(UdfDef udf) {
         return Tuple3.of(udf.getName(), udf.getClasspath(), udf.getOptions());
     }
+
+    private static final class DefaultModelContext implements ModelContext {
+        private final ModelDef modelDef;
+        private final ClassLoader classLoader;
+
+        private DefaultModelContext(ModelDef modelDef, ClassLoader 
classLoader) {
+            this.modelDef = modelDef;
+            this.classLoader = classLoader;
+        }
+
+        @Override
+        public String getModelName() {
+            return modelDef.getName();
+        }
+
+        @Override
+        public Map<String, String> getOptions() {
+            return modelDef.getOptions();
+        }
+
+        @Override
+        public ClassLoader getClassLoader() {
+            return classLoader;
+        }
+    }
 }
diff --git 
a/flink-cdc-composer/src/test/java/org/apache/flink/cdc/composer/flink/FlinkPipelineAiFunctionITCase.java
 
b/flink-cdc-composer/src/test/java/org/apache/flink/cdc/composer/flink/FlinkPipelineAiFunctionITCase.java
new file mode 100644
index 000000000..8aabfaffb
--- /dev/null
+++ 
b/flink-cdc-composer/src/test/java/org/apache/flink/cdc/composer/flink/FlinkPipelineAiFunctionITCase.java
@@ -0,0 +1,197 @@
+/*
+ * 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.flink.cdc.composer.flink;
+
+import org.apache.flink.cdc.common.configuration.Configuration;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.event.CreateTableEvent;
+import org.apache.flink.cdc.common.event.DataChangeEvent;
+import org.apache.flink.cdc.common.event.Event;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.pipeline.PipelineOptions;
+import org.apache.flink.cdc.common.pipeline.SchemaChangeBehavior;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataType;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.composer.PipelineExecution;
+import org.apache.flink.cdc.composer.definition.ModelDef;
+import org.apache.flink.cdc.composer.definition.PipelineDef;
+import org.apache.flink.cdc.composer.definition.SinkDef;
+import org.apache.flink.cdc.composer.definition.SourceDef;
+import org.apache.flink.cdc.composer.definition.TransformDef;
+import org.apache.flink.cdc.connectors.values.ValuesDatabase;
+import org.apache.flink.cdc.connectors.values.factory.ValuesDataFactory;
+import org.apache.flink.cdc.connectors.values.sink.ValuesDataSinkOptions;
+import org.apache.flink.cdc.connectors.values.source.ValuesDataSourceHelper;
+import org.apache.flink.cdc.connectors.values.source.ValuesDataSourceOptions;
+import org.apache.flink.cdc.runtime.typeutils.BinaryRecordDataGenerator;
+import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration;
+import org.apache.flink.test.junit5.MiniClusterExtension;
+
+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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static 
org.apache.flink.configuration.CoreOptions.ALWAYS_PARENT_FIRST_LOADER_PATTERNS_ADDITIONAL;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Integration test for AI functions in the Flink pipeline. */
+class FlinkPipelineAiFunctionITCase {
+
+    private static final int MAX_PARALLELISM = 4;
+
+    private static final org.apache.flink.configuration.Configuration 
MINI_CLUSTER_CONFIG =
+            new org.apache.flink.configuration.Configuration();
+
+    static {
+        MINI_CLUSTER_CONFIG.set(
+                ALWAYS_PARENT_FIRST_LOADER_PATTERNS_ADDITIONAL,
+                Collections.singletonList("org.apache.flink.cdc"));
+    }
+
+    @RegisterExtension
+    static final MiniClusterExtension MINI_CLUSTER_RESOURCE =
+            new MiniClusterExtension(
+                    new MiniClusterResourceConfiguration.Builder()
+                            .setNumberTaskManagers(1)
+                            .setNumberSlotsPerTaskManager(MAX_PARALLELISM)
+                            .setConfiguration(MINI_CLUSTER_CONFIG)
+                            .build());
+
+    private final PrintStream standardOut = System.out;
+    private final ByteArrayOutputStream outCaptor = new 
ByteArrayOutputStream();
+
+    @BeforeEach
+    void init() {
+        System.setOut(new PrintStream(outCaptor));
+        ValuesDatabase.clear();
+    }
+
+    @AfterEach
+    void cleanup() {
+        System.setOut(standardOut);
+    }
+
+    @Test
+    void testAiCompleteInProjection() throws Exception {
+        String[] output =
+                runAiFunctionTest(
+                        "id, content, AI_COMPLETE('testModel', content, 
'Complete the text') AS completed",
+                        List.of(
+                                ModelDef.of(
+                                        "testModel",
+                                        "dummy",
+                                        Collections.singletonMap("debug", 
"true"))));
+        assertThat(output)
+                .contains(
+                        "Dummy model opened.",
+                        
"CreateTableEvent{tableId=default_namespace.default_schema.mytable1, 
schema=columns={`id` INT NOT NULL,`content` STRING,`completed` VARIANT}, 
primaryKeys=id, options=()}",
+                        
"DataChangeEvent{tableId=default_namespace.default_schema.mytable1, before=[], 
after=[1, I love this product, {\"result\":\"dummy response\"}], op=INSERT, 
meta=()}",
+                        "Dummy model closed.");
+    }
+
+    @Test
+    void testAiEmbedInProjection() throws Exception {
+        String[] output =
+                runAiFunctionTest(
+                        "id, AI_EMBED('embedModel', content) AS embedding",
+                        List.of(ModelDef.of("embedModel", "dummy", 
Collections.emptyMap())));
+        assertThat(output)
+                .containsExactly(
+                        
"CreateTableEvent{tableId=default_namespace.default_schema.mytable1, 
schema=columns={`id` INT NOT NULL,`embedding` ARRAY<FLOAT>}, primaryKeys=id, 
options=()}",
+                        
"DataChangeEvent{tableId=default_namespace.default_schema.mytable1, before=[], 
after=[1, [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]], op=INSERT, meta=()}");
+    }
+
+    private String[] runAiFunctionTest(String projection, List<ModelDef> 
models) throws Exception {
+        FlinkPipelineComposer composer = FlinkPipelineComposer.ofMiniCluster();
+
+        // Source: one table with a single row
+        TableId tableId = TableId.tableId("default_namespace", 
"default_schema", "mytable1");
+        Schema schema =
+                Schema.newBuilder()
+                        .physicalColumn("id", DataTypes.INT())
+                        .physicalColumn("content", DataTypes.STRING())
+                        .primaryKey("id")
+                        .build();
+        BinaryRecordDataGenerator generator =
+                new 
BinaryRecordDataGenerator(schema.getColumnDataTypes().toArray(new DataType[0]));
+
+        List<Event> events = new ArrayList<>();
+        events.add(new CreateTableEvent(tableId, schema));
+        events.add(
+                DataChangeEvent.insertEvent(
+                        tableId,
+                        generator.generate(
+                                new Object[] {
+                                    1, BinaryStringData.fromString("I love 
this product")
+                                })));
+        
ValuesDataSourceHelper.setSourceEvents(Collections.singletonList(events));
+
+        Configuration sourceConfig = new Configuration();
+        sourceConfig.set(
+                ValuesDataSourceOptions.EVENT_SET_ID,
+                ValuesDataSourceHelper.EventSetId.CUSTOM_SOURCE_EVENTS);
+        SourceDef sourceDef =
+                new SourceDef(ValuesDataFactory.IDENTIFIER, "Value Source", 
sourceConfig);
+
+        // Sink
+        Configuration sinkConfig = new Configuration();
+        sinkConfig.set(ValuesDataSinkOptions.MATERIALIZED_IN_MEMORY, true);
+        SinkDef sinkDef = new SinkDef(ValuesDataFactory.IDENTIFIER, "Value 
Sink", sinkConfig);
+
+        // Transform
+        TransformDef transformDef =
+                new TransformDef(
+                        "default_namespace.default_schema.mytable1",
+                        projection,
+                        null,
+                        "id",
+                        null,
+                        null,
+                        null,
+                        null);
+
+        // Pipeline
+        Configuration pipelineConfig = new Configuration();
+        pipelineConfig.set(PipelineOptions.PIPELINE_PARALLELISM, 1);
+        pipelineConfig.set(
+                PipelineOptions.PIPELINE_SCHEMA_CHANGE_BEHAVIOR, 
SchemaChangeBehavior.EVOLVE);
+        PipelineDef pipelineDef =
+                new PipelineDef(
+                        sourceDef,
+                        sinkDef,
+                        Collections.emptyList(),
+                        Collections.singletonList(transformDef),
+                        Collections.emptyList(),
+                        models,
+                        pipelineConfig);
+
+        // Execute & capture output
+        PipelineExecution execution = composer.compose(pipelineDef);
+        execution.execute();
+
+        return outCaptor.toString().trim().split("\n");
+    }
+}
diff --git a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml
index 029360af4..52e3dc6aa 100644
--- a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml
+++ b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml
@@ -564,6 +564,16 @@ limitations under the License.
                             </outputDirectory>
                         </artifactItem>
 
+                        <artifactItem>
+                            <groupId>org.apache.flink</groupId>
+                            
<artifactId>flink-cdc-pipeline-model-dummy</artifactId>
+                            <version>${project.version}</version>
+                            <destFileName>dummy-model.jar</destFileName>
+                            <type>jar</type>
+                            
<outputDirectory>${project.build.directory}/dependencies
+                            </outputDirectory>
+                        </artifactItem>
+
                         <artifactItem>
                             <groupId>org.apache.flink</groupId>
                             
<artifactId>flink-cdc-pipeline-connector-mysql</artifactId>
diff --git 
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/AiFunctionE2eITCase.java
 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/AiFunctionE2eITCase.java
new file mode 100644
index 000000000..8bdb7cb9a
--- /dev/null
+++ 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/AiFunctionE2eITCase.java
@@ -0,0 +1,123 @@
+/*
+ * 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.flink.cdc.pipeline.tests;
+
+import org.apache.flink.cdc.common.test.utils.TestUtils;
+import org.apache.flink.cdc.pipeline.tests.utils.PipelineTestEnvironment;
+
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Path;
+import java.time.Duration;
+
+/** E2e tests for AI functions with the dummy model SPI. */
+class AiFunctionE2eITCase extends PipelineTestEnvironment {
+
+    private static final String TABLE_1 = 
"default_namespace.default_schema.table1";
+    private static final String TABLE_2 = 
"default_namespace.default_schema.table2";
+    private static final String DUMMY_JSON = "{\"result\":\"dummy response\"}";
+    private static final String DUMMY_EMBEDDING = "[3.0, 1.0, 4.0, 1.0, 5.0, 
9.0, 2.0, 6.0]";
+
+    @Test
+    void testAiFunctionsWithDummyModel() throws Exception {
+        String pipelineJob =
+                "source:\n"
+                        + "  type: values\n"
+                        + "  event-set.id: SINGLE_SPLIT_MULTI_TABLES\n"
+                        + "\n"
+                        + "sink:\n"
+                        + "  type: values\n"
+                        + "\n"
+                        + "transform:\n"
+                        + "  - source-table: "
+                        + TABLE_1
+                        + "\n"
+                        + "    projection: col1, AI_COMPLETE('myModel', col1, 
'Complete it') AS completed\n"
+                        + "  - source-table: "
+                        + TABLE_2
+                        + "\n"
+                        + "    projection: col1, AI_EMBED('myModel', col1) AS 
embedding\n"
+                        + "\n"
+                        + "pipeline:\n"
+                        + "  parallelism: 1\n"
+                        + "  schema.change.behavior: evolve\n"
+                        + "  model:\n"
+                        + "    name: myModel\n"
+                        + "    type: dummy\n"
+                        + "    options:\n"
+                        + "      debug: true\n";
+
+        Path dummyModelJar = TestUtils.getResource("dummy-model.jar");
+        submitPipelineJob(pipelineJob, dummyModelJar);
+        waitUntilJobFinished(Duration.ofMinutes(3));
+
+        validateResult("Successfully opened AI model client 'myModel'.");
+        validateResult(
+                "CreateTableEvent{tableId="
+                        + TABLE_1
+                        + ", schema=columns={`col1` STRING NOT 
NULL,`completed` VARIANT}, primaryKeys=col1, options=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_1
+                        + ", before=[], after=[1, "
+                        + DUMMY_JSON
+                        + "], op=INSERT, meta=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_1
+                        + ", before=[], after=[2, "
+                        + DUMMY_JSON
+                        + "], op=INSERT, meta=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_1
+                        + ", before=[], after=[3, "
+                        + DUMMY_JSON
+                        + "], op=INSERT, meta=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_1
+                        + ", before=[1, "
+                        + DUMMY_JSON
+                        + "], after=[], op=DELETE, meta=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_1
+                        + ", before=[2, "
+                        + DUMMY_JSON
+                        + "], after=[2, "
+                        + DUMMY_JSON
+                        + "], op=UPDATE, meta=()}");
+
+        validateResult(
+                "CreateTableEvent{tableId="
+                        + TABLE_2
+                        + ", schema=columns={`col1` STRING NOT 
NULL,`embedding` ARRAY<FLOAT>}, primaryKeys=col1, options=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_2
+                        + ", before=[], after=[1, "
+                        + DUMMY_EMBEDDING
+                        + "], op=INSERT, meta=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_2
+                        + ", before=[], after=[2, "
+                        + DUMMY_EMBEDDING
+                        + "], op=INSERT, meta=()}",
+                "DataChangeEvent{tableId="
+                        + TABLE_2
+                        + ", before=[], after=[3, "
+                        + DUMMY_EMBEDDING
+                        + "], op=INSERT, meta=()}");
+        validateResult("Successfully closed AI model client 'myModel'.");
+    }
+}
diff --git 
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/resources/rules/malformed.yaml
 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/resources/rules/malformed.yaml
index ffed7ad9b..6fe4a61e1 100644
--- 
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/resources/rules/malformed.yaml
+++ 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/resources/rules/malformed.yaml
@@ -55,20 +55,3 @@ steps:
     error: |
       YAML UDF block is expecting an array children, but got an OBJECT 
({"name":"addone","classpath":"org.apache.flink.cdc.udf.examples.%s.AddOneFunctionClass"}).
       Perhaps you missed a dash prefix `-`?
-  # Models not an array
-  - type: submit
-    yaml: |
-      source:
-        type: values
-      sink:
-        type: values
-      pipeline:
-        model:
-          model-name: GET_EMBEDDING
-          class-name: OpenAIEmbeddingModel
-          openai.model: text-embedding-3-small
-          openai.host: https://xxxx
-          openai.apikey: abcd1234
-    error: |
-      YAML model block is expecting an array children, but got an OBJECT 
({"model-name":"GET_EMBEDDING","class-name":"OpenAIEmbeddingModel","openai.model":"text-embedding-3-small","openai.host":"https://xxxx","openai.apikey":"abcd1234"}).
-      Perhaps you missed a dash prefix `-`?
diff --git a/flink-cdc-pipeline-model/pom.xml 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/pom.xml
similarity index 59%
copy from flink-cdc-pipeline-model/pom.xml
copy to flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/pom.xml
index e7dba7f6f..d72d93e9a 100644
--- a/flink-cdc-pipeline-model/pom.xml
+++ b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/pom.xml
@@ -19,16 +19,13 @@ limitations under the License.
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <parent>
-        <artifactId>flink-cdc-parent</artifactId>
+        <artifactId>flink-cdc-pipeline-model-parent</artifactId>
         <groupId>org.apache.flink</groupId>
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>flink-cdc-pipeline-model</artifactId>
-    <properties>
-        <langchain4j.version>0.23.0</langchain4j.version>
-    </properties>
+    <artifactId>flink-cdc-pipeline-model-dummy</artifactId>
 
     <dependencies>
         <dependency>
@@ -49,39 +46,6 @@ limitations under the License.
                 </exclusion>
             </exclusions>
         </dependency>
-        <dependency>
-            <groupId>dev.langchain4j</groupId>
-            <artifactId>langchain4j</artifactId>
-            <version>${langchain4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>dev.langchain4j</groupId>
-            <artifactId>langchain4j-open-ai</artifactId>
-            <version>${langchain4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>com.theokanning.openai-gpt3-java</groupId>
-            <artifactId>service</artifactId>
-            <version>0.12.0</version>
-        </dependency>
     </dependencies>
 
-
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-jar-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>test-jar</id>
-                        <goals>
-                            <goal>test-jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
-
-</project>
\ No newline at end of file
+</project>
diff --git 
a/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/java/org/apache/flink/cdc/models/dummy/DummyModelClient.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/java/org/apache/flink/cdc/models/dummy/DummyModelClient.java
new file mode 100644
index 000000000..f4c2b7444
--- /dev/null
+++ 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/java/org/apache/flink/cdc/models/dummy/DummyModelClient.java
@@ -0,0 +1,61 @@
+/*
+ * 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.flink.cdc.models.dummy;
+
+import org.apache.flink.cdc.common.model.AiModelClient;
+import org.apache.flink.cdc.common.model.abilities.SupportsEmbedding;
+import org.apache.flink.cdc.common.model.abilities.SupportsTextGeneration;
+
+/** Deterministic AI model client used by tests. */
+public class DummyModelClient implements AiModelClient, 
SupportsTextGeneration, SupportsEmbedding {
+
+    private static final long serialVersionUID = 1L;
+
+    private final boolean debug;
+
+    public DummyModelClient(boolean debug) {
+        this.debug = debug;
+    }
+
+    @Override
+    public String generate(String systemPrompt, String userInput) {
+        if (debug) {
+            System.out.printf("Received prompt: %s%nUser input: %s%n", 
systemPrompt, userInput);
+        }
+        return "{\"result\":\"dummy response\"}";
+    }
+
+    @Override
+    public float[] embed(String text) {
+        return new float[] {3f, 1f, 4f, 1f, 5f, 9f, 2f, 6f};
+    }
+
+    @Override
+    public void open() {
+        if (debug) {
+            System.out.println("Dummy model opened.");
+        }
+    }
+
+    @Override
+    public void close() {
+        if (debug) {
+            System.out.println("Dummy model closed.");
+        }
+    }
+}
diff --git 
a/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/java/org/apache/flink/cdc/models/dummy/DummyModelClientFactory.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/java/org/apache/flink/cdc/models/dummy/DummyModelClientFactory.java
new file mode 100644
index 000000000..4bf89de02
--- /dev/null
+++ 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/java/org/apache/flink/cdc/models/dummy/DummyModelClientFactory.java
@@ -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.
+ */
+
+package org.apache.flink.cdc.models.dummy;
+
+import org.apache.flink.cdc.common.configuration.ConfigOption;
+import org.apache.flink.cdc.common.configuration.ConfigOptions;
+import org.apache.flink.cdc.common.configuration.Configuration;
+import org.apache.flink.cdc.common.model.AiModelClient;
+import org.apache.flink.cdc.common.model.AiModelClientFactory;
+import org.apache.flink.cdc.common.model.ModelContext;
+
+import java.util.Collections;
+import java.util.Set;
+
+/** Factory for the test-only {@link DummyModelClient}. */
+public class DummyModelClientFactory implements AiModelClientFactory {
+
+    private static final ConfigOption<Boolean> DEBUG =
+            ConfigOptions.key("debug").booleanType().defaultValue(false);
+
+    @Override
+    public String identifier() {
+        return "dummy";
+    }
+
+    @Override
+    public Set<ConfigOption<?>> requiredOptions() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public Set<ConfigOption<?>> optionalOptions() {
+        return Set.of(DEBUG);
+    }
+
+    @Override
+    public AiModelClient createClient(ModelContext context) {
+        boolean debug = Configuration.fromMap(context.getOptions()).get(DEBUG);
+        return new DummyModelClient(debug);
+    }
+}
diff --git 
a/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/resources/META-INF/services/org.apache.flink.cdc.common.factories.Factory
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/resources/META-INF/services/org.apache.flink.cdc.common.factories.Factory
new file mode 100644
index 000000000..c1ed9c43f
--- /dev/null
+++ 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/main/resources/META-INF/services/org.apache.flink.cdc.common.factories.Factory
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.flink.cdc.models.dummy.DummyModelClientFactory
diff --git 
a/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/test/java/org/apache/flink/cdc/models/dummy/DummyModelClientFactoryTest.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/test/java/org/apache/flink/cdc/models/dummy/DummyModelClientFactoryTest.java
new file mode 100644
index 000000000..eaee34182
--- /dev/null
+++ 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-dummy/src/test/java/org/apache/flink/cdc/models/dummy/DummyModelClientFactoryTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.flink.cdc.models.dummy;
+
+import org.apache.flink.cdc.common.configuration.Configuration;
+import org.apache.flink.cdc.common.factories.FactoryHelper;
+import org.apache.flink.cdc.common.model.AiModelClient;
+import org.apache.flink.cdc.common.model.ModelContext;
+import org.apache.flink.table.api.ValidationException;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link DummyModelClientFactory}. */
+class DummyModelClientFactoryTest {
+
+    @Test
+    void testFactoryHelperValidationAndClientCreation() {
+        DummyModelClientFactory factory = new DummyModelClientFactory();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+        Configuration options = 
Configuration.fromMap(Collections.singletonMap("debug", "true"));
+
+        FactoryHelper.createFactoryHelper(
+                        factory,
+                        new FactoryHelper.DefaultContext(options, new 
Configuration(), classLoader))
+                .validate();
+        AiModelClient client =
+                factory.createClient(new TestingModelContext(options.toMap(), 
classLoader));
+
+        assertThat(client).isInstanceOf(DummyModelClient.class);
+    }
+
+    @Test
+    void testUnknownOptionIsRejected() {
+        DummyModelClientFactory factory = new DummyModelClientFactory();
+        Configuration options =
+                
Configuration.fromMap(Collections.singletonMap("unknown-option", "value"));
+
+        assertThatThrownBy(
+                        () ->
+                                FactoryHelper.createFactoryHelper(
+                                                factory,
+                                                new 
FactoryHelper.DefaultContext(
+                                                        options,
+                                                        new Configuration(),
+                                                        Thread.currentThread()
+                                                                
.getContextClassLoader()))
+                                        .validate())
+                .isInstanceOf(ValidationException.class)
+                .hasMessageContaining("Unsupported options");
+    }
+
+    private static class TestingModelContext implements ModelContext {
+        private final Map<String, String> options;
+        private final ClassLoader classLoader;
+
+        private TestingModelContext(Map<String, String> options, ClassLoader 
classLoader) {
+            this.options = options;
+            this.classLoader = classLoader;
+        }
+
+        @Override
+        public String getModelName() {
+            return "dummy-model";
+        }
+
+        @Override
+        public Map<String, String> getOptions() {
+            return options;
+        }
+
+        @Override
+        public ClassLoader getClassLoader() {
+            return classLoader;
+        }
+    }
+}
diff --git a/flink-cdc-pipeline-model/pom.xml 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/pom.xml
similarity index 95%
copy from flink-cdc-pipeline-model/pom.xml
copy to flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/pom.xml
index e7dba7f6f..e0b0c2fd9 100644
--- a/flink-cdc-pipeline-model/pom.xml
+++ b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/pom.xml
@@ -19,13 +19,15 @@ limitations under the License.
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <parent>
-        <artifactId>flink-cdc-parent</artifactId>
+        <artifactId>flink-cdc-pipeline-model-parent</artifactId>
         <groupId>org.apache.flink</groupId>
         <version>${revision}</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
+    <!-- Keep the original artifact coordinates for backward compatibility. -->
     <artifactId>flink-cdc-pipeline-model</artifactId>
+
     <properties>
         <langchain4j.version>0.23.0</langchain4j.version>
     </properties>
@@ -66,7 +68,6 @@ limitations under the License.
         </dependency>
     </dependencies>
 
-
     <build>
         <plugins>
             <plugin>
@@ -84,4 +85,4 @@ limitations under the License.
         </plugins>
     </build>
 
-</project>
\ No newline at end of file
+</project>
diff --git 
a/flink-cdc-pipeline-model/src/main/java/org/apache/flink/cdc/runtime/model/ModelOptions.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/main/java/org/apache/flink/cdc/runtime/model/ModelOptions.java
similarity index 100%
rename from 
flink-cdc-pipeline-model/src/main/java/org/apache/flink/cdc/runtime/model/ModelOptions.java
rename to 
flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/main/java/org/apache/flink/cdc/runtime/model/ModelOptions.java
diff --git 
a/flink-cdc-pipeline-model/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIChatModel.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIChatModel.java
similarity index 100%
rename from 
flink-cdc-pipeline-model/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIChatModel.java
rename to 
flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIChatModel.java
diff --git 
a/flink-cdc-pipeline-model/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIEmbeddingModel.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIEmbeddingModel.java
similarity index 100%
rename from 
flink-cdc-pipeline-model/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIEmbeddingModel.java
rename to 
flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/main/java/org/apache/flink/cdc/runtime/model/OpenAIEmbeddingModel.java
diff --git 
a/flink-cdc-pipeline-model/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIChatModel.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIChatModel.java
similarity index 100%
rename from 
flink-cdc-pipeline-model/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIChatModel.java
rename to 
flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIChatModel.java
diff --git 
a/flink-cdc-pipeline-model/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIEmbeddingModel.java
 
b/flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIEmbeddingModel.java
similarity index 100%
rename from 
flink-cdc-pipeline-model/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIEmbeddingModel.java
rename to 
flink-cdc-pipeline-model/flink-cdc-pipeline-model-legacy/src/test/java/org/apache/flink/cdc/runtime/model/TestOpenAIEmbeddingModel.java
diff --git a/flink-cdc-pipeline-model/pom.xml b/flink-cdc-pipeline-model/pom.xml
index e7dba7f6f..1a3e398b7 100644
--- a/flink-cdc-pipeline-model/pom.xml
+++ b/flink-cdc-pipeline-model/pom.xml
@@ -25,63 +25,12 @@ limitations under the License.
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>flink-cdc-pipeline-model</artifactId>
-    <properties>
-        <langchain4j.version>0.23.0</langchain4j.version>
-    </properties>
+    <artifactId>flink-cdc-pipeline-model-parent</artifactId>
+    <packaging>pom</packaging>
 
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.flink</groupId>
-            <artifactId>flink-cdc-common</artifactId>
-            <version>${project.version}</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.flink</groupId>
-            <artifactId>flink-test-utils-junit</artifactId>
-            <version>${flink.version}</version>
-            <scope>test</scope>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.testcontainers</groupId>
-                    <artifactId>testcontainers</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>dev.langchain4j</groupId>
-            <artifactId>langchain4j</artifactId>
-            <version>${langchain4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>dev.langchain4j</groupId>
-            <artifactId>langchain4j-open-ai</artifactId>
-            <version>${langchain4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>com.theokanning.openai-gpt3-java</groupId>
-            <artifactId>service</artifactId>
-            <version>0.12.0</version>
-        </dependency>
-    </dependencies>
+    <modules>
+        <module>flink-cdc-pipeline-model-legacy</module>
+        <module>flink-cdc-pipeline-model-dummy</module>
+    </modules>
 
-
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-jar-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>test-jar</id>
-                        <goals>
-                            <goal>test-jar</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
-
-</project>
\ No newline at end of file
+</project>
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/ai/AiEmbeddingFunctionDef.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/ai/AiEmbeddingFunctionDef.java
new file mode 100644
index 000000000..3554f604c
--- /dev/null
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/ai/AiEmbeddingFunctionDef.java
@@ -0,0 +1,50 @@
+/*
+ * 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.flink.cdc.runtime.ai;
+
+import org.apache.flink.cdc.common.types.DataType;
+import org.apache.flink.cdc.common.types.DataTypes;
+
+/** Built-in AI embedding function definitions with configurable input and 
output types. */
+public enum AiEmbeddingFunctionDef {
+    AI_EMBED("AI_EMBED", DataTypes.STRING(), 
DataTypes.ARRAY(DataTypes.FLOAT()));
+
+    private final String functionName;
+    private final DataType inputType;
+    private final DataType outputType;
+
+    AiEmbeddingFunctionDef(String functionName, DataType inputType, DataType 
outputType) {
+        this.functionName = functionName;
+        this.inputType = inputType;
+        this.outputType = outputType;
+    }
+
+    public String getFunctionName() {
+        return functionName;
+    }
+
+    /** The type of the input value (e.g. STRING for text embedding). */
+    public DataType getInputType() {
+        return inputType;
+    }
+
+    /** The type of the output value (e.g. ARRAY&lt;FLOAT&gt; for vector 
embedding). */
+    public DataType getOutputType() {
+        return outputType;
+    }
+}
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/ai/AiTextFunctionDef.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/ai/AiTextFunctionDef.java
new file mode 100644
index 000000000..74687617a
--- /dev/null
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/ai/AiTextFunctionDef.java
@@ -0,0 +1,61 @@
+/*
+ * 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.flink.cdc.runtime.ai;
+
+import org.apache.flink.cdc.common.types.DataType;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.common.types.RowType;
+
+/** Built-in AI text generation function definitions. */
+public enum AiTextFunctionDef {
+    AI_COMPLETE(
+            "AI_COMPLETE",
+            RowType.of(new DataType[] {DataTypes.STRING()}, new String[] 
{"systemPrompt"}),
+            RowType.of(new DataType[] {DataTypes.STRING()}, new String[] 
{"result"}),
+            "%s\n");
+
+    private final String functionName;
+    private final RowType inputType;
+    private final RowType outputType;
+    private final String promptTemplate;
+
+    AiTextFunctionDef(
+            String functionName, RowType inputType, RowType outputType, String 
promptTemplate) {
+        this.functionName = functionName;
+        this.inputType = inputType;
+        this.outputType = outputType;
+        this.promptTemplate = promptTemplate;
+    }
+
+    public String getFunctionName() {
+        return functionName;
+    }
+
+    /** Returns the parameter types after the model and input arguments. */
+    public RowType getInputType() {
+        return inputType;
+    }
+
+    public RowType getOutputType() {
+        return outputType;
+    }
+
+    public String buildPrompt(Object... args) {
+        return String.format(promptTemplate, args);
+    }
+}
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/AiFunctions.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/AiFunctions.java
new file mode 100644
index 000000000..7d17c86c4
--- /dev/null
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/AiFunctions.java
@@ -0,0 +1,85 @@
+/*
+ * 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.flink.cdc.runtime.functions.impl;
+
+import org.apache.flink.cdc.common.model.AiModelClient;
+import org.apache.flink.cdc.common.model.abilities.SupportsEmbedding;
+import org.apache.flink.cdc.common.model.abilities.SupportsTextGeneration;
+import org.apache.flink.cdc.common.types.RowType;
+import org.apache.flink.cdc.common.types.variant.BinaryVariant;
+import org.apache.flink.cdc.common.types.variant.BinaryVariantInternalBuilder;
+import org.apache.flink.cdc.runtime.ai.AiTextFunctionDef;
+
+import org.apache.flink.shaded.guava31.com.google.common.primitives.Floats;
+
+import java.io.IOException;
+import java.util.List;
+
+/** Built-in AI functions available to transform expressions. */
+public class AiFunctions {
+
+    private AiFunctions() {}
+
+    public static BinaryVariant aiComplete(AiModelClient model, String input, 
String systemPrompt) {
+        if (!(model instanceof SupportsTextGeneration)) {
+            throw new UnsupportedOperationException(
+                    "Model " + model.getClass().getName() + " does not support 
text generation");
+        }
+
+        AiTextFunctionDef function = AiTextFunctionDef.AI_COMPLETE;
+        String prompt =
+                function.buildPrompt(systemPrompt)
+                        + "\n"
+                        + buildOutputSchemaHint(function.getOutputType());
+        String json = ((SupportsTextGeneration) model).generate(prompt, input);
+        if (json == null) {
+            return null;
+        }
+        try {
+            return BinaryVariantInternalBuilder.parseJson(json, false);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to parse AI response as JSON: " 
+ json, e);
+        }
+    }
+
+    public static List<Float> aiEmbed(AiModelClient model, String input) {
+        if (!(model instanceof SupportsEmbedding)) {
+            throw new UnsupportedOperationException(
+                    "Model " + model.getClass().getName() + " does not support 
embedding");
+        }
+        float[] embedding = ((SupportsEmbedding) model).embed(input);
+        return embedding == null ? null : Floats.asList(embedding);
+    }
+
+    private static String buildOutputSchemaHint(RowType outputType) {
+        StringBuilder builder = new StringBuilder("Return valid JSON with this 
shape:\n{\n");
+        List<String> fieldNames = outputType.getFieldNames();
+        for (int i = 0; i < fieldNames.size(); i++) {
+            builder.append("  \"")
+                    .append(fieldNames.get(i))
+                    .append("\": <")
+                    .append(fieldNames.get(i))
+                    .append(">");
+            if (i < fieldNames.size() - 1) {
+                builder.append(',');
+            }
+            builder.append('\n');
+        }
+        return builder.append('}').toString();
+    }
+}
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperator.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperator.java
index 0a8b63703..9c6552fbe 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperator.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperator.java
@@ -29,6 +29,7 @@ import org.apache.flink.cdc.common.event.DataChangeEvent;
 import org.apache.flink.cdc.common.event.Event;
 import org.apache.flink.cdc.common.event.SchemaChangeEvent;
 import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.model.AiModelClient;
 import org.apache.flink.cdc.common.schema.Schema;
 import org.apache.flink.cdc.common.schema.Selectors;
 import org.apache.flink.cdc.common.udf.UserDefinedFunctionContext;
@@ -41,6 +42,7 @@ import 
org.apache.flink.cdc.runtime.typeutils.BinaryInternalObjectConverter;
 import org.apache.flink.cdc.runtime.typeutils.BinaryRecordDataGenerator;
 import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
 import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.util.FlinkRuntimeException;
 
 import org.apache.flink.shaded.guava31.com.google.common.cache.CacheBuilder;
 import org.apache.flink.shaded.guava31.com.google.common.cache.CacheLoader;
@@ -48,6 +50,9 @@ import 
org.apache.flink.shaded.guava31.com.google.common.cache.LoadingCache;
 import 
org.apache.flink.shaded.guava31.com.google.common.collect.HashBasedTable;
 import org.apache.flink.shaded.guava31.com.google.common.collect.Table;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import javax.annotation.Nullable;
 
 import java.io.Serializable;
@@ -69,6 +74,7 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
         implements OneInputStreamOperator<Event, Event>, Serializable {
 
     private static final long serialVersionUID = 1L;
+    private static final Logger LOG = 
LoggerFactory.getLogger(PostTransformOperator.class);
 
     private final String timezone;
     private final List<TransformRule> transformRules;
@@ -79,6 +85,9 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
     // Tuple3 items are: function name, class path, and extra options.
     private final List<Tuple3<String, String, Map<String, String>>> 
udfFunctions;
 
+    // Serializable AI model clients keyed by model name, e.g. myModel.
+    private final Map<String, AiModelClient> modelClients;
+
     private transient List<PostTransformer> transformers;
     private transient List<UserDefinedFunctionDescriptor> udfDescriptors;
     private transient List<Object> udfFunctionInstances;
@@ -98,13 +107,15 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
     PostTransformOperator(
             List<TransformRule> transformRules,
             String timezone,
-            List<Tuple3<String, String, Map<String, String>>> udfFunctions) {
+            List<Tuple3<String, String, Map<String, String>>> udfFunctions,
+            Map<String, AiModelClient> modelClients) {
         this.timezone = timezone;
         this.transformRules = transformRules;
         this.hasAsteriskMap = new HashMap<>();
         this.projectedColumnsMap = new HashMap<>();
         this.postTransformInfoMap = new ConcurrentHashMap<>();
         this.udfFunctions = udfFunctions;
+        this.modelClients = modelClients;
     }
 
     @Override
@@ -115,6 +126,9 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
         this.projectionProcessors = HashBasedTable.create();
         this.filterProcessors = HashBasedTable.create();
 
+        // Initialize AI model clients
+        initializeAiModelClients();
+
         // Be sure to initialize UDF related fields before creating 
transformers
         initializeUdf();
 
@@ -136,6 +150,7 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
         super.close();
         TransformExpressionCompiler.cleanUp();
         destroyUdf();
+        destroyAiModelClients();
     }
 
     @Override
@@ -447,7 +462,8 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
                             timezone,
                             udfDescriptors,
                             udfFunctionInstances,
-                            postTransformer.getSupportedMetadataColumns()));
+                            postTransformer.getSupportedMetadataColumns(),
+                            modelClients));
         }
         return projectionProcessors.get(tableId, postTransformer);
     }
@@ -472,7 +488,8 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
                                 timezone,
                                 udfDescriptors,
                                 udfFunctionInstances,
-                                
postTransformer.getSupportedMetadataColumns()));
+                                postTransformer.getSupportedMetadataColumns(),
+                                modelClients));
             }
         }
         return filterProcessors.get(tableId, postTransformer);
@@ -558,4 +575,28 @@ public class PostTransformOperator extends 
AbstractStreamOperatorAdapter<Event>
         udfDescriptors.clear();
         udfFunctionInstances.clear();
     }
+
+    private void initializeAiModelClients() {
+        for (Map.Entry<String, AiModelClient> entry : modelClients.entrySet()) 
{
+            try {
+                entry.getValue().open();
+                LOG.info("Successfully opened AI model client '{}'.", 
entry.getKey());
+            } catch (Exception e) {
+                LOG.error("Failed to open AI model client '{}'.", 
entry.getKey(), e);
+                throw new FlinkRuntimeException(
+                        "Failed to initialize AI model: " + entry.getKey(), e);
+            }
+        }
+    }
+
+    private void destroyAiModelClients() {
+        for (Map.Entry<String, AiModelClient> entry : modelClients.entrySet()) 
{
+            try {
+                entry.getValue().close();
+                LOG.info("Successfully closed AI model client '{}'.", 
entry.getKey());
+            } catch (Exception e) {
+                LOG.warn("Failed to close AI model client '{}'.", 
entry.getKey(), e);
+            }
+        }
+    }
 }
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperatorBuilder.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperatorBuilder.java
index 380d9343a..be2e8823b 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperatorBuilder.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/PostTransformOperatorBuilder.java
@@ -18,6 +18,7 @@
 package org.apache.flink.cdc.runtime.operators.transform;
 
 import org.apache.flink.api.java.tuple.Tuple3;
+import org.apache.flink.cdc.common.model.AiModelClient;
 import org.apache.flink.cdc.common.pipeline.PipelineOptions;
 import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
 
@@ -25,6 +26,7 @@ import javax.annotation.Nullable;
 
 import java.time.ZoneId;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -34,6 +36,7 @@ public class PostTransformOperatorBuilder {
     private String timezone;
     private final List<Tuple3<String, String, Map<String, String>>> 
udfFunctions =
             new ArrayList<>();
+    private final Map<String, AiModelClient> modelClients = new 
LinkedHashMap<>();
 
     public PostTransformOperatorBuilder addTransform(
             String tableInclusions,
@@ -111,7 +114,12 @@ public class PostTransformOperatorBuilder {
         return this;
     }
 
+    public PostTransformOperatorBuilder addModelClients(Map<String, 
AiModelClient> clients) {
+        this.modelClients.putAll(clients);
+        return this;
+    }
+
     public PostTransformOperator build() {
-        return new PostTransformOperator(transformRules, timezone, 
udfFunctions);
+        return new PostTransformOperator(transformRules, timezone, 
udfFunctions, modelClients);
     }
 }
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/ProjectionColumnProcessor.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/ProjectionColumnProcessor.java
index db5c37ca2..2dc86b37b 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/ProjectionColumnProcessor.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/ProjectionColumnProcessor.java
@@ -18,6 +18,7 @@
 package org.apache.flink.cdc.runtime.operators.transform;
 
 import org.apache.flink.cdc.common.converter.JavaClassConverter;
+import org.apache.flink.cdc.common.model.AiModelClient;
 import org.apache.flink.cdc.common.schema.Column;
 import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
 import org.apache.flink.cdc.runtime.parser.JaninoCompiler;
@@ -26,6 +27,7 @@ import org.codehaus.janino.ExpressionEvaluator;
 
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -45,6 +47,7 @@ public class ProjectionColumnProcessor {
     private final TransformExpressionKey transformExpressionKey;
     private final Map<String, SupportedMetadataColumn> 
supportedMetadataColumns;
     private final List<Object> udfFunctionInstances;
+    private final Map<String, AiModelClient> modelClients;
     private final ExpressionEvaluator expressionEvaluator;
 
     public ProjectionColumnProcessor(
@@ -53,15 +56,17 @@ public class ProjectionColumnProcessor {
             String timezone,
             List<UserDefinedFunctionDescriptor> udfDescriptors,
             final List<Object> udfFunctionInstances,
-            Map<String, SupportedMetadataColumn> supportedMetadataColumns) {
+            Map<String, SupportedMetadataColumn> supportedMetadataColumns,
+            Map<String, AiModelClient> modelClients) {
         this.tableInfo = tableInfo;
         this.projectionColumn = projectionColumn;
         this.timezone = timezone;
         this.supportedMetadataColumns = supportedMetadataColumns;
+        this.modelClients = modelClients;
         this.transformExpressionKey = generateTransformExpressionKey();
         this.expressionEvaluator =
                 TransformExpressionCompiler.compileExpression(
-                        transformExpressionKey, udfDescriptors);
+                        transformExpressionKey, udfDescriptors, modelClients);
         this.udfFunctionInstances = udfFunctionInstances;
     }
 
@@ -72,13 +77,32 @@ public class ProjectionColumnProcessor {
             List<UserDefinedFunctionDescriptor> udfDescriptors,
             List<Object> udfFunctionInstances,
             Map<String, SupportedMetadataColumn> supportedMetadataColumns) {
+        return of(
+                tableInfo,
+                projectionColumn,
+                timezone,
+                udfDescriptors,
+                udfFunctionInstances,
+                supportedMetadataColumns,
+                Collections.emptyMap());
+    }
+
+    public static ProjectionColumnProcessor of(
+            PostTransformChangeInfo tableInfo,
+            ProjectionColumn projectionColumn,
+            String timezone,
+            List<UserDefinedFunctionDescriptor> udfDescriptors,
+            List<Object> udfFunctionInstances,
+            Map<String, SupportedMetadataColumn> supportedMetadataColumns,
+            Map<String, AiModelClient> modelClients) {
         return new ProjectionColumnProcessor(
                 tableInfo,
                 projectionColumn,
                 timezone,
                 udfDescriptors,
                 udfFunctionInstances,
-                supportedMetadataColumns);
+                supportedMetadataColumns,
+                modelClients);
     }
 
     public Object evaluate(Object[] rowData, TransformContext context) {
@@ -123,6 +147,9 @@ public class ProjectionColumnProcessor {
 
         // 3 - Add UDF function instances
         params.addAll(udfFunctionInstances);
+
+        // 4 - Add AI model client instances
+        params.addAll(modelClients.values());
         return params.toArray();
     }
 
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
index 1085a7df8..776b4af2d 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformExpressionCompiler.java
@@ -18,6 +18,7 @@
 package org.apache.flink.cdc.runtime.operators.transform;
 
 import org.apache.flink.api.common.InvalidProgramException;
+import org.apache.flink.cdc.common.model.AiModelClient;
 import 
org.apache.flink.cdc.runtime.operators.transform.exceptions.TransformException;
 import org.apache.flink.util.FlinkRuntimeException;
 
@@ -30,7 +31,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * The processor of the transform expression. It processes the expression of 
projections and
@@ -54,6 +57,20 @@ public class TransformExpressionCompiler {
     /** Compiles an expression code to a janino {@link ExpressionEvaluator}. */
     public static ExpressionEvaluator compileExpression(
             TransformExpressionKey key, List<UserDefinedFunctionDescriptor> 
udfDescriptors) {
+        return compileExpression(key, udfDescriptors, Collections.emptyMap());
+    }
+
+    /**
+     * Compiles an expression code to a janino {@link ExpressionEvaluator}, 
with additional {@link
+     * AiModelClient} instances appended after UDF instances.
+     *
+     * <p>{@code modelClients} maps model names (e.g. {@code myModel}) to the 
corresponding client
+     * instances.
+     */
+    public static ExpressionEvaluator compileExpression(
+            TransformExpressionKey key,
+            List<UserDefinedFunctionDescriptor> udfDescriptors,
+            Map<String, AiModelClient> modelClients) {
         try {
             return COMPILED_EXPRESSION_CACHE.get(
                     key,
@@ -68,6 +85,11 @@ public class TransformExpressionCompiler {
                             
argumentClasses.add(Class.forName(udfFunction.getClasspath()));
                         }
 
+                        for (String paramName : modelClients.keySet()) {
+                            argumentNames.add(paramName);
+                            argumentClasses.add(AiModelClient.class);
+                        }
+
                         // Input args
                         expressionEvaluator.setParameters(
                                 argumentNames.toArray(new String[0]),
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
index 56ce510c1..6781ea191 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformFilterProcessor.java
@@ -19,6 +19,7 @@ package org.apache.flink.cdc.runtime.operators.transform;
 
 import org.apache.flink.api.java.tuple.Tuple2;
 import org.apache.flink.cdc.common.converter.JavaClassConverter;
+import org.apache.flink.cdc.common.model.AiModelClient;
 import org.apache.flink.cdc.common.schema.Column;
 import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
 import org.apache.flink.cdc.runtime.parser.JaninoCompiler;
@@ -47,6 +48,7 @@ public class TransformFilterProcessor {
     private final String timezone;
     private final List<Object> udfFunctionInstances;
     private final Map<String, SupportedMetadataColumn> 
supportedMetadataColumns;
+    private final Map<String, AiModelClient> modelClients;
 
     private final TransformExpressionKey transformExpressionKey;
     private final ExpressionEvaluator expressionEvaluator;
@@ -58,13 +60,15 @@ public class TransformFilterProcessor {
             String timezone,
             List<UserDefinedFunctionDescriptor> udfDescriptors,
             List<Object> udfFunctionInstances,
-            Map<String, SupportedMetadataColumn> supportedMetadataColumns) {
+            Map<String, SupportedMetadataColumn> supportedMetadataColumns,
+            Map<String, AiModelClient> modelClients) {
         this.isNoOp = isNoOp;
         this.tableInfo = tableInfo;
         this.transformFilter = transformFilter;
         this.timezone = timezone;
         this.udfFunctionInstances = udfFunctionInstances;
         this.supportedMetadataColumns = supportedMetadataColumns;
+        this.modelClients = modelClients;
 
         if (isNoOp) {
             this.transformExpressionKey = null;
@@ -79,12 +83,12 @@ public class TransformFilterProcessor {
                                     .toArray(new SupportedMetadataColumn[0]));
             this.expressionEvaluator =
                     TransformExpressionCompiler.compileExpression(
-                            transformExpressionKey, udfDescriptors);
+                            transformExpressionKey, udfDescriptors, 
modelClients);
         }
     }
 
     public static TransformFilterProcessor ofNoOp() {
-        return new TransformFilterProcessor(true, null, null, null, null, 
null, null);
+        return new TransformFilterProcessor(true, null, null, null, null, 
null, null, null);
     }
 
     public static TransformFilterProcessor of(
@@ -93,7 +97,8 @@ public class TransformFilterProcessor {
             String timezone,
             List<UserDefinedFunctionDescriptor> udfDescriptors,
             List<Object> udfFunctionInstances,
-            SupportedMetadataColumn[] supportedMetadataColumns) {
+            SupportedMetadataColumn[] supportedMetadataColumns,
+            Map<String, AiModelClient> modelClients) {
         Map<String, SupportedMetadataColumn> supportedMetadataColumnsMap = new 
HashMap<>();
         for (SupportedMetadataColumn supportedMetadataColumn : 
supportedMetadataColumns) {
             supportedMetadataColumnsMap.put(
@@ -106,7 +111,8 @@ public class TransformFilterProcessor {
                 timezone,
                 udfDescriptors,
                 udfFunctionInstances,
-                supportedMetadataColumnsMap);
+                supportedMetadataColumnsMap,
+                modelClients);
     }
 
     public boolean test(Object[] preRow, Object[] postRow, TransformContext 
context) {
@@ -209,6 +215,9 @@ public class TransformFilterProcessor {
 
         // 3 - Add UDF function instances
         params.addAll(udfFunctionInstances);
+
+        // 4 - Add AI model client instances
+        params.addAll(modelClients.values());
         return params.toArray();
     }
 
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformProjectionProcessor.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformProjectionProcessor.java
index 231e72875..09fe5fb19 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformProjectionProcessor.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/operators/transform/TransformProjectionProcessor.java
@@ -17,6 +17,7 @@
 
 package org.apache.flink.cdc.runtime.operators.transform;
 
+import org.apache.flink.cdc.common.model.AiModelClient;
 import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
 import org.apache.flink.cdc.common.utils.Preconditions;
 import org.apache.flink.cdc.runtime.parser.TransformParser;
@@ -52,6 +53,7 @@ public class TransformProjectionProcessor {
     private final List<ProjectionColumnProcessor> columnProcessors;
     private final SupportedMetadataColumn[] supportedMetadataColumns;
     private final Map<String, SupportedMetadataColumn> 
supportedMetadataColumnsMap;
+    private final Map<String, AiModelClient> modelClients;
 
     public TransformProjectionProcessor(
             PostTransformChangeInfo changeInfo,
@@ -59,13 +61,15 @@ public class TransformProjectionProcessor {
             String timezone,
             List<UserDefinedFunctionDescriptor> udfDescriptors,
             List<Object> udfFunctionInstances,
-            SupportedMetadataColumn[] supportedMetadataColumns) {
+            SupportedMetadataColumn[] supportedMetadataColumns,
+            Map<String, AiModelClient> modelClients) {
         this.changeInfo = changeInfo;
         this.projectionExpression = projectionExpression;
         this.timezone = timezone;
         this.udfDescriptors = udfDescriptors;
         this.udfFunctionInstances = udfFunctionInstances;
         this.supportedMetadataColumns = supportedMetadataColumns;
+        this.modelClients = modelClients;
 
         // Construct a mapping table ad-hoc to accelerate looking-up
         Map<String, SupportedMetadataColumn> supportedMetadataColumnsMap = new 
HashMap<>();
@@ -105,7 +109,8 @@ public class TransformProjectionProcessor {
                                                 timezone,
                                                 udfDescriptors,
                                                 udfFunctionInstances,
-                                                supportedMetadataColumnsMap))
+                                                supportedMetadataColumnsMap,
+                                                modelClients))
                         .collect(Collectors.toList());
 
         LOG.info("Successfully created projection column processors cache.");
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
index 7dda0a26e..abe2fb8c2 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
@@ -28,6 +28,8 @@ import org.apache.flink.cdc.common.types.DataTypeRoot;
 import org.apache.flink.cdc.common.types.DecimalType;
 import org.apache.flink.cdc.common.utils.Preconditions;
 import org.apache.flink.cdc.common.utils.StringUtils;
+import org.apache.flink.cdc.runtime.ai.AiEmbeddingFunctionDef;
+import org.apache.flink.cdc.runtime.ai.AiTextFunctionDef;
 import 
org.apache.flink.cdc.runtime.operators.transform.UserDefinedFunctionDescriptor;
 import org.apache.flink.cdc.runtime.parser.metadata.MetadataColumns;
 
@@ -117,7 +119,7 @@ public class JaninoCompiler {
     public static final String DEFAULT_TIME_ZONE = "__time_zone__";
 
     private static final String[] BUILTIN_FUNCTION_MODULES = {
-        "Arithmetic", "Casting", "Comparison", "Logical", "String", "Struct", 
"Temporal"
+        "Ai", "Arithmetic", "Casting", "Comparison", "Logical", "String", 
"Struct", "Temporal"
     };
 
     @VisibleForTesting
@@ -858,6 +860,13 @@ public class JaninoCompiler {
         } else if (operationName.equals("NULLIF")) {
             return generateNullIfOperation(context, sqlBasicCall, atoms);
         } else {
+            if (isAiFunction(operationName) && atoms.length >= 1) {
+                if (!(sqlBasicCall.operand(0) instanceof 
SqlCharStringLiteral)) {
+                    throw new ParseException(
+                            "The model argument of an AI function must be a 
string constant.");
+                }
+                rewriteAiFunctionModelArg(atoms);
+            }
             return new Java.MethodInvocation(
                     Location.NOWHERE,
                     null,
@@ -950,6 +959,28 @@ public class JaninoCompiler {
         }
     }
 
+    private static boolean isAiFunction(String upperCaseName) {
+        for (AiTextFunctionDef def : AiTextFunctionDef.values()) {
+            if (def.getFunctionName().equals(upperCaseName)) {
+                return true;
+            }
+        }
+        for (AiEmbeddingFunctionDef def : AiEmbeddingFunctionDef.values()) {
+            if (def.getFunctionName().equals(upperCaseName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static void rewriteAiFunctionModelArg(Java.Rvalue[] atoms) {
+        String modelName = atoms[0].toString();
+        if (modelName.startsWith("\"") && modelName.endsWith("\"")) {
+            modelName = modelName.substring(1, modelName.length() - 1);
+        }
+        atoms[0] = new Java.AmbiguousName(Location.NOWHERE, new String[] 
{modelName});
+    }
+
     private static Java.Rvalue generateTimezoneFreeTemporalFunctionOperation(
             Context context, String operationName) {
         return new Java.MethodInvocation(
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
index 6020245ba..8276d323c 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
@@ -24,6 +24,7 @@ import org.apache.flink.cdc.common.types.DataType;
 import org.apache.flink.cdc.common.utils.Preconditions;
 import org.apache.flink.cdc.runtime.operators.transform.ProjectionColumn;
 import 
org.apache.flink.cdc.runtime.operators.transform.UserDefinedFunctionDescriptor;
+import org.apache.flink.cdc.runtime.parser.metadata.AiFunctionSqlOperatorTable;
 import org.apache.flink.cdc.runtime.parser.metadata.TransformSchemaFactory;
 import org.apache.flink.cdc.runtime.parser.metadata.TransformSqlOperatorTable;
 import org.apache.flink.cdc.runtime.typeutils.CalciteDataTypeConverter;
@@ -47,6 +48,7 @@ import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.impl.ScalarFunctionImpl;
 import org.apache.calcite.sql.SqlBasicCall;
 import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlCharStringLiteral;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlIdentifier;
@@ -164,9 +166,13 @@ public class TransformParser {
                         new CalciteConnectionConfigImpl(new Properties()));
         TransformSqlOperatorTable transformSqlOperatorTable = 
TransformSqlOperatorTable.instance();
         SqlOperatorTable udfOperatorTable = SqlOperatorTables.of(udfFunctions);
+        SqlOperatorTable aiFunctionOperatorTable = 
AiFunctionSqlOperatorTable.create();
         SqlValidator validator =
                 SqlValidatorUtil.newValidator(
-                        SqlOperatorTables.chain(transformSqlOperatorTable, 
udfOperatorTable),
+                        SqlOperatorTables.chain(
+                                transformSqlOperatorTable,
+                                udfOperatorTable,
+                                aiFunctionOperatorTable),
                         calciteCatalogReader,
                         factory,
                         SqlValidator.Config.DEFAULT
@@ -724,6 +730,54 @@ public class TransformParser {
         return parseSelect(statement.toString());
     }
 
+    /** Validates model arguments and references in the supported AI 
functions. */
+    public static void validateAiModelReferences(
+            @Nullable String projection, @Nullable String filter, Set<String> 
declaredModelNames) {
+        if (!isNullOrWhitespaceOnly(projection)) {
+            validateAiModelReferences(parseProjectionExpression(projection), 
declaredModelNames);
+        }
+        if (!isNullOrWhitespaceOnly(filter)) {
+            validateAiModelReferences(parseFilterExpression(filter), 
declaredModelNames);
+        }
+    }
+
+    private static void validateAiModelReferences(SqlNode node, Set<String> 
declaredModelNames) {
+        if (node instanceof SqlCall) {
+            SqlCall call = (SqlCall) node;
+            if (isAiFunction(call.getOperator().getName())) {
+                if (call.operandCount() == 0) {
+                    return;
+                }
+                SqlNode modelArgument = call.operand(0);
+                Preconditions.checkArgument(
+                        modelArgument instanceof SqlCharStringLiteral,
+                        "The model argument of %s must be a string constant, 
but was %s.",
+                        call.getOperator().getName(),
+                        modelArgument);
+                String modelName = ((SqlCharStringLiteral) 
modelArgument).getNlsString().getValue();
+                Preconditions.checkArgument(
+                        declaredModelNames.contains(modelName),
+                        "Model '%s' referenced by %s has not been declared.",
+                        modelName,
+                        call.getOperator().getName());
+            }
+            for (SqlNode operand : call.getOperandList()) {
+                if (operand != null) {
+                    validateAiModelReferences(operand, declaredModelNames);
+                }
+            }
+        } else if (node instanceof SqlNodeList) {
+            for (SqlNode child : (SqlNodeList) node) {
+                validateAiModelReferences(child, declaredModelNames);
+            }
+        }
+    }
+
+    private static boolean isAiFunction(String functionName) {
+        return "AI_COMPLETE".equalsIgnoreCase(functionName)
+                || "AI_EMBED".equalsIgnoreCase(functionName);
+    }
+
     public static boolean hasAsterisk(@Nullable String projection) {
         if (isNullOrWhitespaceOnly(projection)) {
             // Providing an empty projection expression is equivalent to 
writing `*` explicitly.
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/AiFunctionSqlOperatorTable.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/AiFunctionSqlOperatorTable.java
new file mode 100644
index 000000000..c1d1fa995
--- /dev/null
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/AiFunctionSqlOperatorTable.java
@@ -0,0 +1,110 @@
+/*
+ * 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.flink.cdc.runtime.parser.metadata;
+
+import org.apache.flink.cdc.common.types.DataType;
+import org.apache.flink.cdc.common.types.RowType;
+import org.apache.flink.cdc.runtime.ai.AiEmbeddingFunctionDef;
+import org.apache.flink.cdc.runtime.ai.AiTextFunctionDef;
+import org.apache.flink.cdc.runtime.typeutils.CalciteDataTypeConverter;
+
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperatorTable;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.util.SqlOperatorTables;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Creates SqlOperatorTable from {@link AiTextFunctionDef} definitions. */
+public class AiFunctionSqlOperatorTable {
+
+    private AiFunctionSqlOperatorTable() {}
+
+    /** Creates an SqlOperatorTable containing all AI functions defined in 
AiFunctionDef. */
+    public static SqlOperatorTable create() {
+        List<SqlFunction> functions = new ArrayList<>();
+        for (AiTextFunctionDef def : AiTextFunctionDef.values()) {
+            functions.add(createTextSqlFunction(def));
+        }
+        for (AiEmbeddingFunctionDef def : AiEmbeddingFunctionDef.values()) {
+            functions.add(createEmbeddingSqlFunction(def));
+        }
+        return SqlOperatorTables.of(functions);
+    }
+
+    private static SqlFunction createTextSqlFunction(AiTextFunctionDef def) {
+        return new SqlFunction(
+                def.getFunctionName(),
+                SqlKind.OTHER_FUNCTION,
+                ReturnTypes.explicit(SqlTypeName.VARIANT),
+                null,
+                
OperandTypes.family(toSqlTypeFamiliesWithAdditionalParams(def.getInputType())),
+                SqlFunctionCategory.USER_DEFINED_FUNCTION);
+    }
+
+    private static SqlFunction 
createEmbeddingSqlFunction(AiEmbeddingFunctionDef def) {
+        return new SqlFunction(
+                def.getFunctionName(),
+                SqlKind.OTHER_FUNCTION,
+                opBinding ->
+                        CalciteDataTypeConverter.convertCalciteType(
+                                opBinding.getTypeFactory(), 
def.getOutputType()),
+                null,
+                OperandTypes.family(SqlTypeFamily.STRING, 
toSqlTypeFamily(def.getInputType())),
+                SqlFunctionCategory.USER_DEFINED_FUNCTION);
+    }
+
+    /**
+     * Converts inputType to SqlTypeFamily array, prepending additional 
parameters: modelName
+     * (STRING) and input (STRING).
+     */
+    private static SqlTypeFamily[] 
toSqlTypeFamiliesWithAdditionalParams(RowType inputType) {
+        List<SqlTypeFamily> families = new ArrayList<>();
+        families.add(SqlTypeFamily.STRING); // modelName
+        families.add(SqlTypeFamily.STRING); // input
+        for (DataType fieldType : inputType.getFieldTypes()) {
+            families.add(toSqlTypeFamily(fieldType));
+        }
+        return families.toArray(new SqlTypeFamily[0]);
+    }
+
+    private static SqlTypeFamily toSqlTypeFamily(DataType dataType) {
+        switch (dataType.getTypeRoot()) {
+            case VARCHAR:
+            case CHAR:
+                return SqlTypeFamily.STRING;
+            case INTEGER:
+                return SqlTypeFamily.INTEGER;
+            case BIGINT:
+                return SqlTypeFamily.NUMERIC;
+            case FLOAT:
+            case DOUBLE:
+                return SqlTypeFamily.APPROXIMATE_NUMERIC;
+            case BOOLEAN:
+                return SqlTypeFamily.BOOLEAN;
+            default:
+                return SqlTypeFamily.ANY;
+        }
+    }
+}
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/AiFunctionsTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/AiFunctionsTest.java
new file mode 100644
index 000000000..3cf9873fc
--- /dev/null
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/AiFunctionsTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.flink.cdc.runtime.functions.impl;
+
+import org.apache.flink.cdc.common.model.AiModelClient;
+import org.apache.flink.cdc.common.model.abilities.SupportsEmbedding;
+import org.apache.flink.cdc.common.model.abilities.SupportsTextGeneration;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link AiFunctions}. */
+class AiFunctionsTest {
+
+    private static class TestModelClient
+            implements AiModelClient, SupportsTextGeneration, 
SupportsEmbedding {
+
+        private static final long serialVersionUID = 1L;
+
+        private String lastPrompt;
+
+        @Override
+        public String generate(String systemPrompt, String userInput) {
+            lastPrompt = systemPrompt;
+            return "{\"result\":\"ABC\"}";
+        }
+
+        @Override
+        public float[] embed(String text) {
+            return new float[] {0.1f, 0.2f, 0.3f};
+        }
+    }
+
+    private static class UnsupportedModelClient implements AiModelClient {
+        private static final long serialVersionUID = 1L;
+    }
+
+    @Test
+    void testAiFunctions() {
+        TestModelClient model = new TestModelClient();
+
+        assertThat(AiFunctions.aiComplete(model, "input", "Return three 
letters"))
+                .hasToString("{\"result\":\"ABC\"}");
+        assertThat(model.lastPrompt).contains("Return three 
letters").contains("\"result\"");
+        assertThat(AiFunctions.aiEmbed(model, "input")).containsExactly(0.1f, 
0.2f, 0.3f);
+    }
+
+    @Test
+    void testUnsupportedCapabilities() {
+        UnsupportedModelClient model = new UnsupportedModelClient();
+
+        assertThatThrownBy(() -> AiFunctions.aiComplete(model, "input", 
"prompt"))
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("does not support text generation");
+        assertThatThrownBy(() -> AiFunctions.aiEmbed(model, "input"))
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("does not support embedding");
+    }
+
+    @Test
+    void testInvalidJsonResponse() {
+        TestModelClient model =
+                new TestModelClient() {
+                    @Override
+                    public String generate(String systemPrompt, String 
userInput) {
+                        return "not-json";
+                    }
+                };
+
+        assertThatThrownBy(() -> AiFunctions.aiComplete(model, "input", 
"prompt"))
+                .isInstanceOf(RuntimeException.class)
+                .hasMessageContaining("Failed to parse AI response as JSON");
+    }
+}
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/AiFunctionParserTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/AiFunctionParserTest.java
new file mode 100644
index 000000000..e7af51542
--- /dev/null
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/AiFunctionParserTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.flink.cdc.runtime.parser;
+
+import org.apache.flink.cdc.common.schema.Column;
+import org.apache.flink.cdc.common.source.SupportedMetadataColumn;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.runtime.operators.transform.ProjectionColumn;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Parser and Janino tests for the generic AI functions. */
+class AiFunctionParserTest {
+
+    private static final List<Column> COLUMNS =
+            List.of(
+                    Column.physicalColumn("id", DataTypes.INT()),
+                    Column.physicalColumn("content", DataTypes.STRING()));
+
+    @Test
+    void testTranslateAiFunctions() {
+        List<ProjectionColumn> columns =
+                translate(
+                        "AI_COMPLETE('completer', content, 'You are helpful') 
AS completed, "
+                                + "AI_EMBED('embedder', content) AS 
embedding");
+
+        assertThat(columns)
+                .extracting(ProjectionColumn::getScriptExpression)
+                .containsExactly(
+                        "aiComplete(completer, $0, \"You are helpful\")", 
"aiEmbed(embedder, $0)");
+        assertThat(columns)
+                .extracting(ProjectionColumn::getDataType)
+                .containsExactly(DataTypes.VARIANT(), 
DataTypes.ARRAY(DataTypes.FLOAT()));
+    }
+
+    @Test
+    void testModelArgumentMustBeStringConstant() {
+        assertThatThrownBy(
+                        () ->
+                                TransformParser.validateAiModelReferences(
+                                        "AI_COMPLETE(content, content, 
'prompt') AS completed",
+                                        null,
+                                        Set.of("content")))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("must be a string constant");
+    }
+
+    @Test
+    void testReferencedModelMustBeDeclared() {
+        assertThatThrownBy(
+                        () ->
+                                TransformParser.validateAiModelReferences(
+                                        "AI_EMBED('missing', content) AS 
embedding",
+                                        null,
+                                        Set.of("declared")))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Model 'missing'")
+                .hasMessageContaining("has not been declared");
+
+        assertThatCode(
+                        () ->
+                                TransformParser.validateAiModelReferences(
+                                        "AI_EMBED('declared', content) AS 
embedding",
+                                        null,
+                                        Set.of("declared")))
+                .doesNotThrowAnyException();
+    }
+
+    @Test
+    void testFunctionArityValidation() {
+        assertThatThrownBy(() -> translate("AI_EMBED('model') AS embedding"))
+                .hasMessageContaining("Invalid number of arguments to function 
'AI_EMBED'");
+        assertThatThrownBy(() -> translate("AI_COMPLETE('model', content) AS 
completed"))
+                .hasMessageContaining("Invalid number of arguments to function 
'AI_COMPLETE'");
+        assertThatThrownBy(() -> translate("AI_COMPLETE() AS completed"))
+                .hasMessageContaining("Invalid number of arguments to function 
'AI_COMPLETE'");
+        assertThatCode(
+                        () ->
+                                TransformParser.validateAiModelReferences(
+                                        "AI_COMPLETE() AS completed", null, 
Collections.emptySet()))
+                .doesNotThrowAnyException();
+    }
+
+    private List<ProjectionColumn> translate(String expression) {
+        return TransformParser.generateProjectionColumns(
+                expression, COLUMNS, Collections.emptyList(), new 
SupportedMetadataColumn[0]);
+    }
+}

Reply via email to