This is an automated email from the ASF dual-hosted git repository.
liuhongyu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 810e986cad [type:feat] add ai proxy plugin (#5938)
810e986cad is described below
commit 810e986cadbed357270bdc76d3f65d67fd074bb6
Author: aias00 <[email protected]>
AuthorDate: Fri Feb 21 14:33:42 2025 +0800
[type:feat] add ai proxy plugin (#5938)
* [type:feature] ai proxy plugin
* [type:feature] ai proxy plugin
* [type:feature] ai proxy plugin
* [type:feature] ai proxy plugin
* [type:feature] ai proxy plugin
* [type:feature] ai proxy plugin
* [type:feature] ai proxy plugin
---
db/init/mysql/schema.sql | 1 +
.../src/main/resources/sql-script/h2/schema.sql | 19 ++
shenyu-bootstrap/pom.xml | 8 +
.../apache/shenyu/common/constant/Constants.java | 15 ++
.../common/dto/convert/plugin/AiProxyConfig.java | 243 +++++++++++++++++++++
.../common/dto/convert/rule/AiProxyHandle.java | 243 +++++++++++++++++++++
.../dto/convert/rule/GeneralContextHandle.java | 2 +-
.../shenyu/common/dto/convert/rule/MockHandle.java | 2 +-
.../shenyu/common/enums/AiModelProviderEnum.java | 79 +++++++
.../org/apache/shenyu/common/enums/PluginEnum.java | 5 +
shenyu-plugin/pom.xml | 1 +
shenyu-plugin/shenyu-plugin-ai-proxy/pom.xml | 49 +++++
.../shenyu/plugin/ai/proxy/AiProxyPlugin.java | 77 +++++++
.../ai/proxy/handler/AiProxyPluginHandler.java | 73 +++++++
.../shenyu/plugin/ai/proxy/strategy/AiModel.java | 30 +++
.../plugin/ai/proxy/strategy/AiModelFactory.java | 47 ++++
.../plugin/ai/proxy/strategy/openai/OpenAI.java | 161 ++++++++++++++
.../shenyu-spring-boot-starter-plugin/pom.xml | 1 +
.../pom.xml | 35 +++
.../ai/proxy/AiProxyPluginConfiguration.java | 54 +++++
.../src/main/resources/META-INF/spring.factories | 19 ++
.../src/main/resources/META-INF/spring.provides | 18 ++
...rk.boot.autoconfigure.AutoConfiguration.imports | 18 ++
23 files changed, 1198 insertions(+), 2 deletions(-)
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index d744628f11..94d0697be7 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -923,6 +923,7 @@ INSERT INTO `plugin` VALUES ('42', 'tcp', NULL, 'Proxy',
320, 1, '2023-05-30 18:
INSERT INTO `plugin` VALUES ('43', 'loggingHuaweiLts',
'{\"totalSizeInBytes\":\"104857600\",\"maxBlockMs\":\"0\",\"ioThreadCount\":\"1\",\"batchSizeThresholdInBytes\":\"524288\",\"batchCountThreshold\":\"4096\",\"lingerMs\":\"2000\",\"retries\":\"100\",\"baseRetryBackoffMs\":\"100\",\"maxRetryBackoffMs\":\"100\",\"enableLocalTest\":\"true\",\"setGiveUpExtraLongSingleLog\":\"false\"}',
'Logging', 177, 0, '2023-07-05 14:03:53.686', '2023-07-06 12:42:07.234', NULL);
INSERT INTO `plugin` VALUES ('44', 'basicAuth',
'{\"defaultHandleJson\":\"{\\\"authorization\\\":\\\"test:test123\\\"}\"}',
'Authentication', 150, 0, '2022-07-24 19:00:00', '2022-07-24 19:00:00', null);
INSERT INTO `plugin` VALUES ('45', 'loggingRabbitMQ',
'{\"host\":\"127.0.0.1\",\"port\":5672,\"password\":\"admin\",\"username\":\"admin\",\"exchangeName\":\"exchange.logging.plugin\",\"queueName\":\"queue.logging.plugin\",\"routingKey\":\"topic.logging\",\"virtualHost\":\"/\",\"exchangeType\":\"direct\",\"durable\":\"true\",\"exclusive\":\"false\",\"autoDelete\":\"false\"}',
'Logging', 171, 0, '2023-11-06 15:49:56.454', '2023-11-10 10:40:58.447', NULL);
+INSERT INTO `plugin` VALUES ('50', 'aiProxy',
'{\"host\":\"127.0.0.1\",\"port\":5672,\"password\":\"admin\",\"username\":\"admin\",\"exchangeName\":\"exchange.logging.plugin\",\"queueName\":\"queue.logging.plugin\",\"routingKey\":\"topic.logging\",\"virtualHost\":\"/\",\"exchangeType\":\"direct\",\"durable\":\"true\",\"exclusive\":\"false\",\"autoDelete\":\"false\"}',
'Ai', 171, 0, '2023-11-06 15:49:56.454', '2023-11-10 10:40:58.447', NULL);
-- ----------------------------
-- Table structure for plugin_handle
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index 4f7b671ae8..d6cdaff353 100755
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -522,6 +522,12 @@ INSERT IGNORE INTO `shenyu_dict` (`id`,
`type`,`dict_code`, `dict_name`, `dict_v
INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737478',
'rewriteMetaData', 'REWRITE_META_DATA', 'true', 'true', '', 4, 1);
INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737479',
'rewriteMetaData', 'REWRITE_META_DATA', 'false', 'false', '', 4, 1);
+INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737480',
'provider', 'PROVIDER_TYPE_OPENAI', 'OpenAI', 'OpenAI', 'OpenAI', 0, 1);
+INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737481',
'provider', 'PROVIDER_TYPE_DEEPSEEK', 'DeepSeek', 'DeepSeek', 'OpenAI', 1, 1);
+INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737482',
'provider', 'PROVIDER_TYPE_MOONSHOT', 'Moonshot', 'Moonshot', 'Moonshot', 2, 1);
+INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737483',
'provider', 'PROVIDER_TYPE_OPENAPI', 'OpenAPI', 'OpenAPI', 'OpenAPI', 3, 1);
+INSERT IGNORE INTO `shenyu_dict` (`id`, `type`,`dict_code`, `dict_name`,
`dict_value`, `desc`, `sort`, `enabled`) VALUES ('1679002911061737484',
'provider', 'PROVIDER_TYPE_ALIYUN', 'ALiYun', 'ALiYun', 'ALiYun', 4, 1);
+
/*plugin*/
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `enabled`) VALUES
('1','sign','Authentication', 20, '0');
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`,`config`,`enabled`)
VALUES ('2','waf', 'Authentication', 50,'{"model":"black"}','0');
@@ -565,6 +571,8 @@ INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`,
`enabled`) VALUES ('4
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`,
`enabled`) VALUES ('43', 'loggingHuaweiLts', 'Logging', 177, '{
"totalSizeInBytes":"104857600","maxBlockMs":"0","ioThreadCount":"1","batchSizeThresholdInBytes":"524288","batchCountThreshold":"4096","lingerMs":"2000","retries":"100","baseRetryBackoffMs":"100","maxRetryBackoffMs":"100","enableLocalTest":"true","setGiveUpExtraLongSingleLog":"false"}',
'0');
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`,
`enabled`) VALUES ('44', 'basicAuth', 'Authentication', 500,
'{"defaultHandleJson":"{\"authorization\":\"test:test123\"}"}', '0');
INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`,
`enabled`) VALUES ('45', 'loggingRabbitMQ', 'Logging', 171,
'{"host":"127.0.0.1","port":5672,"password":"admin","username":"admin","exchangeName":"exchange.logging.plugin","queueName":"queue.logging.plugin","routingKey":"topic.logging","virtualHost":"/","exchangeType":"direct","durable":"true","exclusive":"false","autoDelete":"false"}',
'0');
+
+INSERT IGNORE INTO `plugin` (`id`, `name`, `role`, `sort`, `config`,
`enabled`) VALUES ('50', 'aiProxy', 'Ai', 171,
'{"provider":"OpenAI","baseUrl":"https://api.openai.com/v1/chat/completions","model":"gpt-4o-mini","apiKey":"your_api_key","temperature":"0.5","maxTokens":"1000","stream":"false","prompt":""}',
'0');
/*insert plugin_handle data for sentinel*/
INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1529402613195784246', '10', 'flowRuleGrade', 'flowRuleGrade', 3, 2, 8,
'{"required":"1","defaultValue":"1","rule":""}');
INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1529402613199978496', '10', 'flowRuleControlBehavior',
'flowRuleControlBehavior', 3, 2, 5,
'{"required":"1","defaultValue":"0","rule":""}');
@@ -926,6 +934,16 @@ INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,
INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507032', '19', 'handleType', 'handleType', 2, 3, 1,
'{"required":"0","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507033', '50', 'provider', 'provider', 3, 3, 1,
'{"required":"1","defaultValue":"OpenAI","placeholder":"provider","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507034', '50', 'baseUrl', 'baseUrl', 2, 3, 2,
'{"required":"1","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507036', '50', 'model', 'model', 2, 3, 3,
'{"required":"1","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507035', '50', 'apiKey', 'apiKey', 2, 3, 4,
'{"required":"1","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507037', '50', 'temperature', 'temperature', 2, 3, 5,
'{"required":"0","rule":"", "placeholder":"optional,0,0.01~1"}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507038', '50', 'maxTokens', 'maxTokens', 2, 3, 6,
'{"required":"0","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507039', '50', 'stream', 'stream', 3, 3, 7,
'{"defaultValue":"false","rule":""}');
+INSERT IGNORE INTO plugin_handle (`id`,
`plugin_id`,`field`,`label`,`data_type`,`type`,`sort`,`ext_obj`) VALUES
('1722804548510507040', '50', 'prompt', 'prompt', 2, 3, 8,
'{"required":"0","rule":""}');
+
+
/** insert resource for resource */
INSERT IGNORE INTO `resource` (`id`, `parent_id`, `title`, `name`, `url`,
`component`, `resource_type`, `sort`, `icon`, `is_leaf`, `is_route`, `perms`,
`status`)
VALUES('1346775491550474240','','SHENYU.MENU.PLUGIN.LIST','plug','/plug','PluginList','0','0','dashboard','0','0','','1');
@@ -1348,6 +1366,7 @@ INSERT IGNORE INTO `namespace_plugin_rel`
(`id`,`namespace_id`,`plugin_id`, `con
INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`,
`config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES
('1801816010882822184','649330b6-c2d7-4edc-be8e-8a54df9eb385','6',
'{"register":"zookeeper://localhost:2181","multiSelectorHandle":"1","threadpool":"shared","corethreads":0,"threads":2147483647,"queues":0}',
310, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000');
INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`,
`config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES
('1801816010882822185','649330b6-c2d7-4edc-be8e-8a54df9eb385','8',
'{"enabled":false,"registerType":"eureka","serverLists":"http://localhost:8761/eureka","props":
{}}', 200, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000');
INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`,
`config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES
('1801816010882822186','649330b6-c2d7-4edc-be8e-8a54df9eb385','9', NULL, 130,
0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000');
+INSERT IGNORE INTO `namespace_plugin_rel` (`id`,`namespace_id`,`plugin_id`,
`config`, `sort`, `enabled`, `date_created`, `date_updated`) VALUES
('1801816010882822187','649330b6-c2d7-4edc-be8e-8a54df9eb385','50',
'{"provider":"OpenAI","baseUrl":"https://api.openai.com/v1/chat/completions","model":"gpt-4o-mini","apiKey":"your_api_key","temperature":"0.5","maxTokens":"1000","stream":"false","prompt":""}',
171, 0, '2022-05-25 18:02:53.000', '2022-05-25 18:02:53.000');
diff --git a/shenyu-bootstrap/pom.xml b/shenyu-bootstrap/pom.xml
index c02c89ec23..874274d16d 100644
--- a/shenyu-bootstrap/pom.xml
+++ b/shenyu-bootstrap/pom.xml
@@ -239,6 +239,14 @@
</dependency>
<!--Tcp Plugin end-->
+ <!--Ai proxy Plugin Start-->
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-spring-boot-starter-plugin-ai-proxy</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <!--Ai proxy Plugin end-->
+
<!--shenyu sofa plugin start-->
<dependency>
<groupId>com.alipay.sofa</groupId>
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/constant/Constants.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/constant/Constants.java
index 3cd0c7d16f..c6713166b8 100644
---
a/shenyu-common/src/main/java/org/apache/shenyu/common/constant/Constants.java
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/constant/Constants.java
@@ -930,6 +930,21 @@ public interface Constants {
* The constant preserve host.
*/
String PRESERVE_HOST = "preserveHost";
+
+ /**
+ * The constant model.
+ */
+ String MODEL = "model";
+
+ /**
+ * The constant stream.
+ */
+ String STREAM = "stream";
+
+ /**
+ * The constant prompt.
+ */
+ String PROMPT = "prompt";
/**
* String q.
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/plugin/AiProxyConfig.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/plugin/AiProxyConfig.java
new file mode 100644
index 0000000000..7fedc8881c
--- /dev/null
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/plugin/AiProxyConfig.java
@@ -0,0 +1,243 @@
+/*
+ * 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.shenyu.common.dto.convert.plugin;
+
+import java.util.Objects;
+
+/**
+ * this is Ai Proxy plugin config.
+ */
+public class AiProxyConfig {
+
+ /**
+ * provider.
+ */
+ private String provider;
+
+ /**
+ * base url.
+ */
+ private String baseUrl;
+
+ /**
+ * api key.
+ */
+ private String apiKey;
+
+ /**
+ * model.
+ */
+ private String model;
+
+ /**
+ * temperature.
+ */
+ private Double temperature = 0.8;
+
+ /**
+ * max tokens.
+ */
+ private Integer maxTokens;
+
+ /**
+ * prompt.
+ */
+ private String prompt;
+
+ /**
+ * stream.
+ */
+ private Boolean stream = false;
+
+ /**
+ * get provider.
+ *
+ * @return provider
+ */
+ public String getProvider() {
+ return provider;
+ }
+
+ /**
+ * set provider.
+ *
+ * @param provider provider
+ */
+ public void setProvider(final String provider) {
+ this.provider = provider;
+ }
+
+ /**
+ * get base url.
+ *
+ * @return base url
+ */
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ /**
+ * set base url.
+ *
+ * @param baseUrl base url
+ */
+ public void setBaseUrl(final String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ /**
+ * get api key.
+ *
+ * @return api key
+ */
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ /**
+ * set api key.
+ *
+ * @param apiKey api key
+ */
+ public void setApiKey(final String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ /**
+ * get model.
+ *
+ * @return model
+ */
+ public String getModel() {
+ return model;
+ }
+
+ /**
+ * set model.
+ *
+ * @param model model
+ */
+ public void setModel(final String model) {
+ this.model = model;
+ }
+
+ /**
+ * get temperature.
+ *
+ * @return temperature
+ */
+ public Double getTemperature() {
+ return temperature;
+ }
+
+ /**
+ * set temperature.
+ *
+ * @param temperature temperature
+ */
+ public void setTemperature(final Double temperature) {
+ this.temperature = temperature;
+ }
+
+ /**
+ * get max tokens.
+ *
+ * @return max tokens
+ */
+ public Integer getMaxTokens() {
+ return maxTokens;
+ }
+
+ /**
+ * set max tokens.
+ *
+ * @param maxTokens max tokens
+ */
+ public void setMaxTokens(final Integer maxTokens) {
+ this.maxTokens = maxTokens;
+ }
+
+ /**
+ * get prompt.
+ *
+ * @return prompt
+ */
+ public String getPrompt() {
+ return prompt;
+ }
+
+ /**
+ * set prompt.
+ *
+ * @param prompt prompt
+ */
+ public void setPrompt(final String prompt) {
+ this.prompt = prompt;
+ }
+
+ /**
+ * get stream.
+ *
+ * @return stream
+ */
+ public Boolean getStream() {
+ return stream;
+ }
+
+ /**
+ * set stream.
+ *
+ * @param stream stream
+ */
+ public void setStream(final Boolean stream) {
+ this.stream = stream;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (Objects.isNull(o) || getClass() != o.getClass()) {
+ return false;
+ }
+ AiProxyConfig that = (AiProxyConfig) o;
+ return Objects.equals(provider, that.provider)
+ && Objects.equals(baseUrl, that.baseUrl)
+ && Objects.equals(apiKey, that.apiKey)
+ && Objects.equals(model, that.model)
+ && Objects.equals(temperature, that.temperature)
+ && Objects.equals(maxTokens, that.maxTokens)
+ && Objects.equals(prompt, that.prompt)
+ && Objects.equals(stream, that.stream);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(provider, baseUrl, apiKey, model, temperature,
maxTokens, prompt, stream);
+ }
+
+ @Override
+ public String toString() {
+ return "AiProxyHandle{" + "provider='" + provider + '\'' + "baseUrl='"
+ baseUrl + '\'' + ", apiKey='" + apiKey
+ + '\'' + ", model='" + model
+ + '\''
+ + ", temperature=" + temperature + ", maxTokens=" + maxTokens
+ + ", prompt='" + prompt + '\'' + ", stream=" + stream + '}';
+ }
+}
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/AiProxyHandle.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/AiProxyHandle.java
new file mode 100644
index 0000000000..d9cf581a9d
--- /dev/null
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/AiProxyHandle.java
@@ -0,0 +1,243 @@
+/*
+ * 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.shenyu.common.dto.convert.rule;
+
+import java.util.Objects;
+
+/**
+ * this is Ai Proxy plugin handle.
+ */
+public class AiProxyHandle {
+
+ /**
+ * type.
+ */
+ private String type;
+
+ /**
+ * base url.
+ */
+ private String baseUrl;
+
+ /**
+ * api key.
+ */
+ private String apiKey;
+
+ /**
+ * model.
+ */
+ private String model;
+
+ /**
+ * temperature.
+ */
+ private Double temperature;
+
+ /**
+ * max tokens.
+ */
+ private Integer maxTokens;
+
+ /**
+ * prompt.
+ */
+ private String prompt;
+
+ /**
+ * stream.
+ */
+ private Boolean stream = false;
+
+ /**
+ * get type.
+ *
+ * @return type
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * set type.
+ *
+ * @param type type
+ */
+ public void setType(final String type) {
+ this.type = type;
+ }
+
+ /**
+ * get base url.
+ *
+ * @return base url
+ */
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ /**
+ * set base url.
+ *
+ * @param baseUrl base url
+ */
+ public void setBaseUrl(final String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ /**
+ * get api key.
+ *
+ * @return api key
+ */
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ /**
+ * set api key.
+ *
+ * @param apiKey api key
+ */
+ public void setApiKey(final String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ /**
+ * get model.
+ *
+ * @return model
+ */
+ public String getModel() {
+ return model;
+ }
+
+ /**
+ * set model.
+ *
+ * @param model model
+ */
+ public void setModel(final String model) {
+ this.model = model;
+ }
+
+ /**
+ * get temperature.
+ *
+ * @return temperature
+ */
+ public Double getTemperature() {
+ return temperature;
+ }
+
+ /**
+ * set temperature.
+ *
+ * @param temperature temperature
+ */
+ public void setTemperature(final Double temperature) {
+ this.temperature = temperature;
+ }
+
+ /**
+ * get max tokens.
+ *
+ * @return max tokens
+ */
+ public Integer getMaxTokens() {
+ return maxTokens;
+ }
+
+ /**
+ * set max tokens.
+ *
+ * @param maxTokens max tokens
+ */
+ public void setMaxTokens(final Integer maxTokens) {
+ this.maxTokens = maxTokens;
+ }
+
+ /**
+ * get prompt.
+ *
+ * @return prompt
+ */
+ public String getPrompt() {
+ return prompt;
+ }
+
+ /**
+ * set prompt.
+ *
+ * @param prompt prompt
+ */
+ public void setPrompt(final String prompt) {
+ this.prompt = prompt;
+ }
+
+ /**
+ * get stream.
+ *
+ * @return stream
+ */
+ public Boolean getStream() {
+ return stream;
+ }
+
+ /**
+ * set stream.
+ *
+ * @param stream stream
+ */
+ public void setStream(final Boolean stream) {
+ this.stream = stream;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (Objects.isNull(o) || getClass() != o.getClass()) {
+ return false;
+ }
+ AiProxyHandle that = (AiProxyHandle) o;
+ return Objects.equals(type, that.type)
+ && Objects.equals(baseUrl, that.baseUrl)
+ && Objects.equals(apiKey, that.apiKey)
+ && Objects.equals(model, that.model)
+ && Objects.equals(temperature, that.temperature)
+ && Objects.equals(maxTokens, that.maxTokens)
+ && Objects.equals(prompt, that.prompt)
+ && Objects.equals(stream, that.stream);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, baseUrl, apiKey, model, temperature,
maxTokens, prompt, stream);
+ }
+
+ @Override
+ public String toString() {
+ return "AiProxyHandle{" + "type='" + type + '\'' + "baseUrl='" +
baseUrl + '\'' + ", apiKey='" + apiKey + '\''
+ + ", model='" + model
+ + '\''
+ + ", temperature=" + temperature + ", maxTokens=" + maxTokens
+ + ", prompt='" + prompt + '\'' + ", stream=" + stream + '}';
+ }
+}
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/GeneralContextHandle.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/GeneralContextHandle.java
index a95e33167a..c8b7d00b51 100644
---
a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/GeneralContextHandle.java
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/GeneralContextHandle.java
@@ -20,7 +20,7 @@ package org.apache.shenyu.common.dto.convert.rule;
import java.util.Objects;
/**
- * this is RequestHandle plugin handle.
+ * this is GeneralContextHandle plugin handle.
*/
public class GeneralContextHandle {
/**
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/MockHandle.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/MockHandle.java
index 0391eb7490..79f0445007 100644
---
a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/MockHandle.java
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/convert/rule/MockHandle.java
@@ -20,7 +20,7 @@ package org.apache.shenyu.common.dto.convert.rule;
import java.util.Objects;
/**
- * this is RequestHandle plugin handle.
+ * this is MockHandle plugin handle.
*/
public class MockHandle {
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/AiModelProviderEnum.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/AiModelProviderEnum.java
new file mode 100644
index 0000000000..fae76609cd
--- /dev/null
+++
b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/AiModelProviderEnum.java
@@ -0,0 +1,79 @@
+/*
+ * 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.shenyu.common.enums;
+
+/**
+ * The Ai model provider enum.
+ */
+public enum AiModelProviderEnum {
+
+ /**
+ * OpenAI.
+ */
+ OPEN_AI("OpenAI"),
+
+ /**
+ * DeepSeek.
+ */
+ DEEP_SEEK("DeepSeek"),
+
+ /**
+ * ALiYun.
+ */
+ ALIYUN("ALiYun"),
+
+ /**
+ * OpenAPI.
+ */
+ OPEN_API("OpenAPI"),
+
+ /**
+ * Moonshot.
+ */
+ MOONSHOT("Moonshot");
+
+ private final String name;
+
+ AiModelProviderEnum(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * get name.
+ *
+ * @return name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * get model by name.
+ *
+ * @param name name
+ * @return AiModelProviderEnum
+ */
+ public static AiModelProviderEnum getByName(final String name) {
+ for (AiModelProviderEnum value : AiModelProviderEnum.values()) {
+ if (value.getName().equals(name)) {
+ return value;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java
b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java
index 398e3a65ed..c588aaa937 100644
--- a/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java
+++ b/shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java
@@ -266,6 +266,11 @@ public enum PluginEnum {
* Key-auth plugin enum.
*/
KEY_AUTH(430, 0, "keyAuth"),
+
+ /**
+ * Ai-proxy plugin enum.
+ */
+ AI_PROXY(440, 0, "aiProxy"),
/**
* Basic-auth plugin enum.
diff --git a/shenyu-plugin/pom.xml b/shenyu-plugin/pom.xml
index 2b6d7dcf5f..5f9b5c8ef9 100644
--- a/shenyu-plugin/pom.xml
+++ b/shenyu-plugin/pom.xml
@@ -49,6 +49,7 @@
<module>shenyu-plugin-proxy</module>
<module>shenyu-plugin-security</module>
<module>shenyu-plugin-fault-tolerance</module>
+ <module>shenyu-plugin-ai-proxy</module>
</modules>
<dependencies>
diff --git a/shenyu-plugin/shenyu-plugin-ai-proxy/pom.xml
b/shenyu-plugin/shenyu-plugin-ai-proxy/pom.xml
new file mode 100644
index 0000000000..510a4b9b4c
--- /dev/null
+++ b/shenyu-plugin/shenyu-plugin-ai-proxy/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
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>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-plugin</artifactId>
+ <version>2.7.1-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>shenyu-plugin-ai-proxy</artifactId>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-plugin-base</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-http</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.projectreactor.netty</groupId>
+ <artifactId>reactor-netty</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/AiProxyPlugin.java
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/AiProxyPlugin.java
new file mode 100644
index 0000000000..ec6a4c72fa
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/AiProxyPlugin.java
@@ -0,0 +1,77 @@
+/*
+ * 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.shenyu.plugin.ai.proxy;
+
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.dto.convert.plugin.AiProxyConfig;
+import org.apache.shenyu.common.enums.AiModelProviderEnum;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.utils.Singleton;
+import org.apache.shenyu.plugin.ai.proxy.strategy.AiModel;
+import org.apache.shenyu.plugin.ai.proxy.strategy.AiModelFactory;
+import org.apache.shenyu.plugin.api.ShenyuPluginChain;
+import org.apache.shenyu.plugin.base.AbstractShenyuPlugin;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+/**
+ * this is ai proxy plugin.
+ */
+public class AiProxyPlugin extends AbstractShenyuPlugin {
+
+ @Override
+ protected Mono<Void> doExecute(final ServerWebExchange exchange, final
ShenyuPluginChain chain,
+ final SelectorData selector, final RuleData rule) {
+ AiProxyConfig aiProxyConfig = Singleton.INST.get(AiProxyConfig.class);
+ if (Objects.isNull(aiProxyConfig)) {
+ return chain.execute(exchange);
+ }
+
+ return DataBufferUtils.join(exchange.getRequest().getBody())
+ .flatMap(dataBuffer -> {
+ byte[] bytes = new byte[dataBuffer.readableByteCount()];
+ dataBuffer.read(bytes);
+ DataBufferUtils.release(dataBuffer);
+ String requestBody = new String(bytes,
StandardCharsets.UTF_8);
+
+ // choose the model by provider
+ String provider = aiProxyConfig.getProvider();
+ AiModelProviderEnum providerEnum =
AiModelProviderEnum.getByName(provider);
+ assert Objects.nonNull(providerEnum);
+ AiModel aiModel =
AiModelFactory.createAiModel(providerEnum);
+ assert Objects.nonNull(aiModel);
+ return aiModel.invoke(aiProxyConfig, exchange,
requestBody);
+ });
+ }
+
+
+ @Override
+ public int getOrder() {
+ return PluginEnum.AI_PROXY.getCode();
+ }
+
+ @Override
+ public String named() {
+ return PluginEnum.AI_PROXY.getName();
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/handler/AiProxyPluginHandler.java
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/handler/AiProxyPluginHandler.java
new file mode 100644
index 0000000000..e7dff639b5
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/handler/AiProxyPluginHandler.java
@@ -0,0 +1,73 @@
+/*
+ * 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.shenyu.plugin.ai.proxy.handler;
+
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.convert.plugin.AiProxyConfig;
+import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.common.utils.Singleton;
+import org.apache.shenyu.plugin.base.cache.CommonHandleCache;
+import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
+import org.apache.shenyu.plugin.base.utils.BeanHolder;
+import org.apache.shenyu.plugin.base.utils.CacheKeyUtils;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * this is ai proxy plugin.
+ */
+public class AiProxyPluginHandler implements PluginDataHandler {
+
+ public static final Supplier<CommonHandleCache<String, AiProxyHandle>>
CACHED_HANDLE = new BeanHolder<>(CommonHandleCache::new);
+
+ @Override
+ public void handlerPlugin(final PluginData pluginData) {
+ if (Objects.nonNull(pluginData) && pluginData.getEnabled()) {
+ AiProxyConfig aiProxyConfig =
GsonUtils.getInstance().fromJson(pluginData.getConfig(), AiProxyConfig.class);
+ if (Objects.isNull(aiProxyConfig)) {
+ return;
+ }
+ Singleton.INST.single(AiProxyConfig.class, aiProxyConfig);
+ }
+ }
+
+ @Override
+ public void handlerRule(final RuleData ruleData) {
+ Optional.ofNullable(ruleData.getHandle())
+ .ifPresent(s -> {
+ AiProxyHandle aiProxyHandle =
GsonUtils.getInstance().fromJson(s, AiProxyHandle.class);
+
CACHED_HANDLE.get().cachedHandle(CacheKeyUtils.INST.getKey(ruleData),
aiProxyHandle);
+ });
+ }
+
+ @Override
+ public void removeRule(final RuleData ruleData) {
+ Optional.ofNullable(ruleData)
+ .ifPresent(s ->
CACHED_HANDLE.get().removeHandle(CacheKeyUtils.INST.getKey(ruleData)));
+ }
+
+ @Override
+ public String pluginNamed() {
+ return PluginEnum.AI_PROXY.getName();
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/AiModel.java
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/AiModel.java
new file mode 100644
index 0000000000..d08332c60b
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/AiModel.java
@@ -0,0 +1,30 @@
+/*
+ * 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.shenyu.plugin.ai.proxy.strategy;
+
+import org.apache.shenyu.common.dto.convert.plugin.AiProxyConfig;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+/**
+ * The interface Ai model.
+ */
+public interface AiModel {
+
+ Mono<Void> invoke(AiProxyConfig aiProxyConfig, ServerWebExchange exchange,
String requestBody);
+}
diff --git
a/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/AiModelFactory.java
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/AiModelFactory.java
new file mode 100644
index 0000000000..324ace7d92
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/AiModelFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.shenyu.plugin.ai.proxy.strategy;
+
+import org.apache.shenyu.common.enums.AiModelProviderEnum;
+import org.apache.shenyu.plugin.ai.proxy.strategy.openai.OpenAI;
+
+import java.util.Objects;
+
+/**
+ * The interface Ai model.
+ */
+public final class AiModelFactory {
+
+ private AiModelFactory() {
+ }
+
+ /**
+ * Create ai model.
+ *
+ * @param provider the provider
+ * @return the ai model provider
+ */
+ public static AiModel createAiModel(final AiModelProviderEnum provider) {
+ if (Objects.isNull(provider)) {
+ return null;
+ }
+ return switch (provider) {
+ case OPEN_AI, DEEP_SEEK, ALIYUN, OPEN_API, MOONSHOT -> new
OpenAI();
+ };
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/openai/OpenAI.java
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/openai/OpenAI.java
new file mode 100644
index 0000000000..1e1c41ce53
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/strategy/openai/OpenAI.java
@@ -0,0 +1,161 @@
+/*
+ * 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.shenyu.plugin.ai.proxy.strategy.openai;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okio.BufferedSource;
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.dto.convert.plugin.AiProxyConfig;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.plugin.ai.proxy.strategy.AiModel;
+import org.jetbrains.annotations.NotNull;
+import org.reactivestreams.Publisher;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The OpenAI model.
+ */
+public class OpenAI implements AiModel {
+
+ private final OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(5, TimeUnit.MINUTES)
+ .readTimeout(5, TimeUnit.MINUTES)
+ .build();
+
+ @Override
+ public Mono<Void> invoke(final AiProxyConfig aiProxyConfig, final
ServerWebExchange exchange,
+ final String requestBody) {
+ Map<String, Object> paramMap =
GsonUtils.getInstance().convertToMap(requestBody);
+ paramMap.put(Constants.MODEL, aiProxyConfig.getModel());
+ paramMap.put(Constants.STREAM, aiProxyConfig.getStream());
+ paramMap.put(Constants.PROMPT, aiProxyConfig.getPrompt());
+
+ RequestBody body =
RequestBody.create(MediaType.parse("application/json"),
+ GsonUtils.getInstance().toJson(paramMap));
+ Request request = buildRequest(aiProxyConfig, body);
+
+ return aiProxyConfig.getStream()
+ ? handleStreamResponse(exchange, request)
+ : handleNormalResponse(exchange, request);
+ }
+
+ private Request buildRequest(final AiProxyConfig config, final RequestBody
body) {
+ return new Request.Builder()
+ .url(config.getBaseUrl())
+ .post(body)
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Authorization", "Bearer " + config.getApiKey())
+ .build();
+ }
+
+ private Mono<Void> handleStreamResponse(final ServerWebExchange exchange,
final Request request) {
+ ServerHttpResponse exchangeResponse = exchange.getResponse();
+ exchangeResponse.getHeaders().set(HttpHeaders.CONTENT_TYPE,
+ org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE);
+ exchangeResponse.getHeaders().set(HttpHeaders.CACHE_CONTROL,
"no-cache");
+ exchangeResponse.getHeaders().set(HttpHeaders.CONNECTION,
"keep-alive");
+
+ return exchangeResponse.writeAndFlushWith(
+ Flux.<Publisher<DataBuffer>>create(
+ sink -> client.newCall(request).enqueue(new
StreamResponseCallback(sink, exchangeResponse)),
+ FluxSink.OverflowStrategy.BUFFER));
+ }
+
+ private Mono<Void> handleNormalResponse(final ServerWebExchange exchange,
final Request request) {
+ return Mono.create(sink -> {
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ sink.error(new IOException("Request failed: " + response));
+ return;
+ }
+
+ String responseBody = response.body().string();
+
exchange.getResponse().getHeaders().setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
+ exchange.getResponse().writeWith(
+
Mono.just(exchange.getResponse().bufferFactory()
+
.wrap(responseBody.getBytes(StandardCharsets.UTF_8))))
+ .subscribe(v -> sink.success(), sink::error);
+ } catch (IOException e) {
+ sink.error(e);
+ }
+ });
+ }
+
+ private record StreamResponseCallback(FluxSink<Publisher<DataBuffer>>
sink, ServerHttpResponse response) implements Callback {
+
+ @Override
+ public void onFailure(@NotNull final Call call, @NotNull final
IOException e) {
+ sink.error(e);
+ }
+
+ @Override
+ public void onResponse(@NotNull final Call call, @NotNull final
Response response) {
+ try (ResponseBody responseBody = response.body()) {
+ if (!response.isSuccessful() || Objects.isNull(responseBody)) {
+ sink.error(new IOException("Request failed: " + response));
+ return;
+ }
+
+ processStreamResponse(responseBody);
+ sink.complete();
+ } catch (IOException e) {
+ sink.error(e);
+ }
+ }
+
+ private void processStreamResponse(final ResponseBody responseBody)
throws IOException {
+ BufferedSource source = responseBody.source();
+ while (!source.exhausted()) {
+ String line = source.readUtf8Line();
+ if (Objects.isNull(line)) {
+ break;
+ }
+
+ if (line.startsWith("data: ")) {
+ String data = line.substring(6).trim();
+ if (!"[DONE]".equals(data)) {
+ DataBuffer buffer = response.bufferFactory()
+ .wrap((data +
"\n").getBytes(StandardCharsets.UTF_8));
+ sink.next(Flux.just(buffer));
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml
index b7efde2f96..48737633dd 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/pom.xml
@@ -74,5 +74,6 @@
<module>shenyu-spring-boot-starter-plugin-transform</module>
<module>shenyu-spring-boot-starter-plugin-logging-huawei-lts</module>
<module>shenyu-spring-boot-starter-plugin-basic-auth</module>
+ <module>shenyu-spring-boot-starter-plugin-ai-proxy</module>
</modules>
</project>
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/pom.xml
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/pom.xml
new file mode 100644
index 0000000000..218e3c8a43
--- /dev/null
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
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>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-spring-boot-starter-plugin</artifactId>
+ <version>2.7.1-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>shenyu-spring-boot-starter-plugin-ai-proxy</artifactId>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.shenyu</groupId>
+ <artifactId>shenyu-plugin-ai-proxy</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfiguration.java
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfiguration.java
new file mode 100644
index 0000000000..70e4c0b493
--- /dev/null
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfiguration.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shenyu.springboot.starter.plugin.ai.proxy;
+
+import org.apache.shenyu.plugin.ai.proxy.AiProxyPlugin;
+import org.apache.shenyu.plugin.ai.proxy.handler.AiProxyPluginHandler;
+import org.apache.shenyu.plugin.api.ShenyuPlugin;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * The type ai proxy plugin configuration.
+ */
+@Configuration
+@ConditionalOnProperty(value = {"shenyu.plugins.ai.proxy.enabled"},
havingValue = "true", matchIfMissing = true)
+public class AiProxyPluginConfiguration {
+
+ /**
+ * Ai proxy plugin.
+ *
+ * @return the shenyu plugin
+ */
+ @Bean
+ public ShenyuPlugin aiProxyPlugin() {
+ return new AiProxyPlugin();
+ }
+
+ /**
+ * Ai proxy plugin handler.
+ *
+ * @return the shenyu plugin handler
+ */
+ @Bean
+ public AiProxyPluginHandler aiProxyPluginHandler() {
+ return new AiProxyPluginHandler();
+ }
+
+}
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring.factories
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..6bd1519a01
--- /dev/null
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,19 @@
+#
+# 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.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.apache.shenyu.springboot.starter.plugin.ai.proxy.AiProxyPluginConfiguration
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring.provides
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring.provides
new file mode 100644
index 0000000000..dcbab4a10e
--- /dev/null
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring.provides
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+provides: shenyu-spring-boot-starter-plugin-ai-proxy
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000000..43cf97f204
--- /dev/null
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,18 @@
+#
+# 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.shenyu.springboot.starter.plugin.ai.proxy.AiProxyPluginConfiguration