This is an automated email from the ASF dual-hosted git repository.
Aias00 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 ab5eeb9fae [ISSUE #6470] Apply AI Proxy plugin-level defaults at
runtime (#7676)
ab5eeb9fae is described below
commit ab5eeb9faed697e43ca35c0dc1d2e5566c427fe4
Author: redyyang <[email protected]>
AuthorDate: Tue Oct 20 12:39:02 2026 +0800
[ISSUE #6470] Apply AI Proxy plugin-level defaults at runtime (#7676)
* fix(ai-proxy): apply plugin-level configuration defaults (#6470)
* fix(ai-proxy): clear defaults when plugin configuration is malformed
---------
Co-authored-by: redyyang <[email protected]>
---
.../shenyu-plugin-ai-proxy/README.md | 32 +++++++++
.../enhanced/handler/AiProxyPluginHandler.java | 37 +++++++++-
.../enhanced/service/AiProxyConfigService.java | 15 +++-
.../enhanced/AiProxyHttpCompatibilityTest.java | 41 ++++++++++-
.../ai/proxy/enhanced/AiProxyPluginTest.java | 2 +-
.../enhanced/handler/AiProxyPluginHandlerTest.java | 84 +++++++++++++++++++++-
.../enhanced/service/AiProxyConfigServiceTest.java | 47 ++++++++++--
.../ai/proxy/AiProxyPluginConfiguration.java | 4 +-
.../ai/proxy/AiProxyPluginConfigurationTest.java | 20 ++++++
9 files changed, 267 insertions(+), 15 deletions(-)
diff --git a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
index 477822d853..a705013ecb 100644
--- a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
+++ b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
@@ -56,6 +56,38 @@ a library codec alone does not define its endpoint or
authentication contract.
Stream state is allocated per subscription, including retries. Finalization
runs only on normal completion, never cancellation or transport failure.
+## Plugin defaults and selector overrides
+
+AI Proxy accepts defaults in the plugin's existing `config` JSON, synchronized
+from Admin. For example:
+
+```json
+{
+ "provider": "OpenAI",
+ "baseUrl": "https://api.openai.com",
+ "apiKey": "<upstream-api-key>",
+ "model": "gpt-4o-mini",
+ "maxTokens": 1024
+}
+```
+
+A selector can then use `{}` to inherit these connection/model settings, or
+supply a partial handle such as `{"model":"another-model"}`. Non-null selector
+values take precedence. Existing selector defaults for temperature (`0.8`),
+stream (`false`) and protocol (`openai-chat`) retain their current precedence;
+set those selector fields explicitly when a route needs different behavior.
+
+Plugin updates replace the defaults rather than retaining omitted fields.
+Blank, `null` or `{}` configuration clears the previous connection/model
values;
+disabling or deleting the plugin also clears its defaults. Each request
receives
+a copy, so request-specific proxy credentials do not alter later requests.
+Malformed plugin configuration clears the defaults and logs a warning without
+interrupting configuration synchronization. A later valid update restores them.
+Defaults belong to the handler/resolver instance wired by the starter; separate
+application contexts do not share plugin configuration through a process-wide
+`Singleton`. Programmatic construction must pass the same
`AiProxyPluginHandler`
+to `new AiProxyConfigService(handler)` and the plugin.
+
## Configuration and use
Add the existing AI Proxy starter to the gateway. Its configuration registers
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandler.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandler.java
index 1c2b13afb2..8335cd859d 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandler.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandler.java
@@ -17,16 +17,21 @@
package org.apache.shenyu.plugin.ai.proxy.enhanced.handler;
+import com.google.gson.JsonParseException;
+import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.common.constant.Constants;
import org.apache.shenyu.common.dto.PluginData;
import org.apache.shenyu.common.dto.SelectorData;
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.plugin.ai.common.config.AiCommonConfig;
import org.apache.shenyu.plugin.ai.proxy.enhanced.cache.AiProxyApiKeyCache;
import org.apache.shenyu.plugin.base.cache.CommonHandleCache;
import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
import org.apache.shenyu.plugin.base.utils.CacheKeyUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Objects;
@@ -35,16 +40,42 @@ import java.util.Objects;
*/
public class AiProxyPluginHandler implements PluginDataHandler {
+ private static final Logger LOG =
LoggerFactory.getLogger(AiProxyPluginHandler.class);
+
private final CommonHandleCache<String, AiProxyHandle>
selectorCachedHandle = new CommonHandleCache<>();
+ private volatile AiCommonConfig globalConfig;
+
public AiProxyPluginHandler() {
}
@Override
public void handlerPlugin(final PluginData pluginData) {
- // Note: The logic for handling global plugin configuration with
Singleton has been removed
- // as it's part of the legacy pattern we are moving away from.
- // No global plugin configuration handling is performed.
+ if (Objects.isNull(pluginData)) {
+ return;
+ }
+ try {
+ globalConfig = Boolean.FALSE.equals(pluginData.getEnabled()) ||
StringUtils.isBlank(pluginData.getConfig())
+ ? null :
GsonUtils.getInstance().fromJson(pluginData.getConfig(), AiCommonConfig.class);
+ } catch (JsonParseException e) {
+ globalConfig = null;
+ LOG.warn("Invalid configuration for plugin {}; cleared global
defaults", pluginData.getName());
+ }
+ }
+
+ @Override
+ public void removePlugin(final PluginData pluginData) {
+ globalConfig = null;
+ }
+
+ /**
+ * Return a request-local copy of the latest plugin defaults.
+ *
+ * @return global configuration, or null when cleared or disabled
+ */
+ public AiCommonConfig getGlobalConfig() {
+ AiCommonConfig snapshot = globalConfig;
+ return Objects.isNull(snapshot) ? null : new AiCommonConfig(snapshot);
}
@Override
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigService.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigService.java
index b52eab6de0..525f5b6e4b 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigService.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigService.java
@@ -21,8 +21,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
import org.apache.shenyu.common.utils.JsonUtils;
-import org.apache.shenyu.common.utils.Singleton;
import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.handler.AiProxyPluginHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,6 +38,17 @@ public class AiProxyConfigService {
private static final String FALLBACK_CONFIG = "fallbackConfig";
+ private final AiProxyPluginHandler pluginHandler;
+
+ /**
+ * Create a resolver using the same handler that receives plugin
configuration updates.
+ *
+ * @param pluginHandler plugin configuration handler
+ */
+ public AiProxyConfigService(final AiProxyPluginHandler pluginHandler) {
+ this.pluginHandler = pluginHandler;
+ }
+
/**
* Resolves the primary configuration for the AI call by merging global and
* selector-level settings.
@@ -46,7 +57,7 @@ public class AiProxyConfigService {
* @return the primary AiCommonConfig
*/
public AiCommonConfig resolvePrimaryConfig(final AiProxyHandle handle) {
- final AiCommonConfig globalConfig =
Singleton.INST.get(AiCommonConfig.class);
+ final AiCommonConfig globalConfig = pluginHandler.getGlobalConfig();
final AiCommonConfig primaryConfig = new AiCommonConfig();
if (Objects.nonNull(globalConfig)) {
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
index f3e2616b76..475c2196f0 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyHttpCompatibilityTest.java
@@ -20,6 +20,7 @@ package org.apache.shenyu.plugin.ai.proxy.enhanced;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.dto.PluginData;
import org.apache.shenyu.common.dto.RuleData;
import org.apache.shenyu.common.dto.SelectorData;
import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
@@ -204,6 +205,40 @@ class AiProxyHttpCompatibilityTest {
}
}
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void appliesPluginDefaultsAndSelectorOverridesThroughRealHttp(final
boolean override) throws Exception {
+ AtomicReference<String> forwarded = new AtomicReference<>();
+ AtomicReference<String> authorization = new AtomicReference<>();
+ DisposableServer server =
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) -> {
+ authorization.set(request.requestHeaders().get("Authorization"));
+ return request.receive().aggregate().asString().flatMap(body -> {
+ forwarded.set(body);
+ return response.header("Content-Type",
"application/json").sendString(Flux.just(CHAT_REPLY)).then();
+ });
+ }).bindNow(TIMEOUT);
+ try {
+ AiProxyPluginHandler handler = new AiProxyPluginHandler();
+ PluginData global = new PluginData();
+ global.setConfig(MAPPER.createObjectNode().put("provider",
"OpenAI").put("baseUrl", "http://127.0.0.1:" + server.port())
+ .put("model", "global-model").put("apiKey",
"global-key").put("maxTokens", 64).toString());
+ handler.handlerPlugin(global);
+ SelectorData selector = selector();
+ selector.setHandle(override ?
"{\"model\":\"selector-model\",\"apiKey\":\"selector-key\"}" : "{}");
+ handler.handlerSelector(selector);
+ MockServerWebExchange exchange = exchange(false);
+ StepVerifier.create(plugin(handler).doExecute(exchange,
mock(ShenyuPluginChain.class), selector, new RuleData())).verifyComplete();
+ assertEquals(200, exchange.getResponse().getStatusCode().value());
+ assertEquals("Bearer " + (override ? "selector-key" :
"global-key"), authorization.get());
+ JsonNode upstream = MAPPER.readTree(forwarded.get());
+ assertEquals(override ? "selector-model" : "global-model",
upstream.get("model").asText());
+ assertEquals(64, upstream.get("max_completion_tokens").asInt());
+ assertEquals(CHAT_REPLY,
exchange.getResponse().getBodyAsString().block(TIMEOUT));
+ } finally {
+ server.disposeNow(TIMEOUT);
+ }
+ }
+
private AiProxyPlugin plugin(final DisposableServer server, final boolean
stream) {
return plugin(server, stream, OpenAiChat.NAME);
}
@@ -218,9 +253,13 @@ class AiProxyHttpCompatibilityTest {
handle.setProtocol(protocol);
handle.setModel("model");
handler.getSelectorCachedHandle().cachedHandle(CacheKeyUtils.INST.getKey("http-selector",
Constants.DEFAULT_RULE), handle);
+ return plugin(handler);
+ }
+
+ private AiProxyPlugin plugin(final AiProxyPluginHandler handler) {
AiProxyEngine engine = new AiProxyEngine(new
AiProxyProtocolFactory(List.of(new OpenAiChat(), new OpenAiResponses())), new
AiProxyProviderFactory(List.of(new OpenAi())),
new WebClientAiProxyTransport(WebClient.builder().build()),
Duration.ZERO);
- return new AiProxyPlugin(new AiProxyConfigService(), engine, handler);
+ return new AiProxyPlugin(new AiProxyConfigService(handler), engine,
handler);
}
private SelectorData selector() {
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPluginTest.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPluginTest.java
index bda2653b1f..a18ff12a23 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPluginTest.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPluginTest.java
@@ -283,7 +283,7 @@ class AiProxyPluginTest {
requests.add(request);
return transport.execute(request);
}, Duration.ZERO);
- return new AiProxyPlugin(new AiProxyConfigService(), engine, handler);
+ return new AiProxyPlugin(new AiProxyConfigService(handler), engine,
handler);
}
private Mono<Void> execute(final AiProxyPlugin plugin) {
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandlerTest.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandlerTest.java
index 5aa2a7a5ed..0a79250c48 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandlerTest.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/handler/AiProxyPluginHandlerTest.java
@@ -18,13 +18,20 @@
package org.apache.shenyu.plugin.ai.proxy.enhanced.handler;
import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.dto.PluginData;
import org.apache.shenyu.common.dto.SelectorData;
import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.service.AiProxyConfigService;
import org.apache.shenyu.plugin.base.utils.CacheKeyUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -115,7 +122,82 @@ class AiProxyPluginHandlerTest {
}
@Test
- void testHandlerPluginDoesNothing() {
+ void testNullPluginUpdateIsIgnored() {
handler.handlerPlugin(null);
}
+
+ @Test
+ void testGlobalConfigurationIsReplacedAndCopied() {
+
handler.handlerPlugin(plugin("{\"provider\":\"OpenAI\",\"apiKey\":\"first-key\",\"model\":\"first-model\"}"));
+ AiCommonConfig first = handler.getGlobalConfig();
+ assertEquals("first-model", first.getModel());
+ first.setApiKey("request-key");
+ assertEquals("first-key", handler.getGlobalConfig().getApiKey());
+ handler.handlerPlugin(plugin("{\"model\":\"second-model\"}"));
+ assertEquals("second-model", handler.getGlobalConfig().getModel());
+ assertNull(handler.getGlobalConfig().getApiKey());
+ assertNull(handler.getGlobalConfig().getProvider());
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {" ", "null", "{}"})
+ void testClearingPluginConfigurationRemovesOldDefaults(final String
config) {
+
handler.handlerPlugin(plugin("{\"apiKey\":\"old-key\",\"model\":\"old-model\"}"));
+ handler.handlerPlugin(plugin(config));
+ AiCommonConfig cleared = new
AiProxyConfigService(handler).resolvePrimaryConfig(null);
+ assertNull(cleared.getApiKey());
+ assertNull(cleared.getModel());
+ }
+
+ @Test
+ void testDisableReenableAndRemovePlugin() {
+ PluginData data = plugin("{\"model\":\"global-model\"}");
+ handler.handlerPlugin(data);
+ data.setEnabled(false);
+ handler.handlerPlugin(data);
+ assertNull(handler.getGlobalConfig());
+ data.setEnabled(true);
+ handler.handlerPlugin(data);
+ assertEquals("global-model", handler.getGlobalConfig().getModel());
+ handler.removePlugin(data);
+ assertNull(handler.getGlobalConfig());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"not-json", "{", "[]",
"{\"maxTokens\":\"invalid\"}"})
+ void testInvalidPluginConfigurationClearsDefaultsAndAllowsRecovery(final
String config) {
+
handler.handlerPlugin(plugin("{\"apiKey\":\"old-key\",\"model\":\"old-model\"}"));
+ assertNotNull(handler.getGlobalConfig());
+
+ assertDoesNotThrow(() -> handler.handlerPlugin(plugin(config)));
+
+ assertNull(handler.getGlobalConfig());
+ AiCommonConfig cleared = new
AiProxyConfigService(handler).resolvePrimaryConfig(null);
+ assertNull(cleared.getApiKey());
+ assertNull(cleared.getModel());
+
+
handler.handlerPlugin(plugin("{\"apiKey\":\"new-key\",\"model\":\"new-model\"}"));
+ assertEquals("new-key", handler.getGlobalConfig().getApiKey());
+ assertEquals("new-model", handler.getGlobalConfig().getModel());
+ }
+
+ @Test
+ void testHandlerInstancesDoNotShareGlobalConfiguration() {
+ AiProxyPluginHandler other = new AiProxyPluginHandler();
+ handler.handlerPlugin(plugin("{\"model\":\"first-model\"}"));
+ assertNull(other.getGlobalConfig());
+ other.handlerPlugin(plugin("{\"model\":\"second-model\"}"));
+ assertEquals("first-model", handler.getGlobalConfig().getModel());
+ assertEquals("second-model", other.getGlobalConfig().getModel());
+ handler.removePlugin(null);
+ assertEquals("second-model", other.getGlobalConfig().getModel());
+ }
+
+ private PluginData plugin(final String config) {
+ PluginData data = new PluginData();
+ data.setName(PluginEnum.AI_PROXY.getName());
+ data.setConfig(config);
+ return data;
+ }
}
diff --git
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigServiceTest.java
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigServiceTest.java
index 6381c8a87e..b35a6982ee 100644
---
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigServiceTest.java
+++
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigServiceTest.java
@@ -18,7 +18,9 @@
package org.apache.shenyu.plugin.ai.proxy.enhanced.service;
import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
-import org.apache.shenyu.common.utils.Singleton;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.handler.AiProxyPluginHandler;
import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -29,6 +31,7 @@ import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(MockitoExtension.class)
@@ -36,17 +39,21 @@ public class AiProxyConfigServiceTest {
private AiProxyConfigService configService;
+ private AiProxyPluginHandler pluginHandler;
+
@BeforeEach
void setUp() {
- configService = new AiProxyConfigService();
- // Note: Singleton has no public clear method, state is managed per
test by re-singling.
+ pluginHandler = new AiProxyPluginHandler();
+ configService = new AiProxyConfigService(pluginHandler);
}
@Test
void testResolvePrimaryConfigGlobalOnly() {
AiCommonConfig globalConfig = new AiCommonConfig();
globalConfig.setModel("global-model");
- Singleton.INST.single(AiCommonConfig.class, globalConfig);
+ PluginData pluginData = new PluginData();
+ pluginData.setConfig(GsonUtils.getInstance().toJson(globalConfig));
+ pluginHandler.handlerPlugin(pluginData);
AiProxyHandle handle = new AiProxyHandle();
AiCommonConfig result = configService.resolvePrimaryConfig(handle);
@@ -71,7 +78,9 @@ public class AiProxyConfigServiceTest {
AiCommonConfig globalConfig = new AiCommonConfig();
globalConfig.setProvider("global-provider");
globalConfig.setModel("global-model");
- Singleton.INST.single(AiCommonConfig.class, globalConfig);
+ PluginData pluginData = new PluginData();
+ pluginData.setConfig(GsonUtils.getInstance().toJson(globalConfig));
+ pluginHandler.handlerPlugin(pluginData);
AiProxyHandle handle = new AiProxyHandle();
handle.setModel("handle-model");
@@ -220,4 +229,32 @@ public class AiProxyConfigServiceTest {
String result = configService.extractPrompt(requestBody);
assertEquals(requestBody, result);
}
+
+ @Test
+ void testConfigUpdatesReplaceDefaultsWithoutMutatingResolvedRequests() {
+ PluginData pluginData = new PluginData();
+
pluginData.setConfig("{\"provider\":\"OpenAI\",\"apiKey\":\"first-key\",\"model\":\"first-model\"}");
+ pluginHandler.handlerPlugin(pluginData);
+ AiCommonConfig first = configService.resolvePrimaryConfig(null);
+
pluginData.setConfig("{\"provider\":\"OpenAI\",\"model\":\"second-model\"}");
+ pluginHandler.handlerPlugin(pluginData);
+ AiCommonConfig second = configService.resolvePrimaryConfig(null);
+ assertEquals("first-model", first.getModel());
+ assertEquals("first-key", first.getApiKey());
+ assertEquals("second-model", second.getModel());
+ assertNull(second.getApiKey());
+ pluginHandler.removePlugin(pluginData);
+ assertNull(configService.resolvePrimaryConfig(null).getProvider());
+ assertNull(configService.resolvePrimaryConfig(null).getModel());
+ }
+
+ @Test
+ void testRequestCredentialOverridesDoNotChangePluginDefaults() {
+ PluginData pluginData = new PluginData();
+ pluginData.setConfig("{\"apiKey\":\"global-key\"}");
+ pluginHandler.handlerPlugin(pluginData);
+ AiCommonConfig resolved = configService.resolvePrimaryConfig(null);
+ resolved.setApiKey("per-request-key");
+ assertEquals("global-key",
configService.resolvePrimaryConfig(null).getApiKey());
+ }
}
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
index 79170c723c..a80f1dd00c 100644
---
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
@@ -87,8 +87,8 @@ public class AiProxyPluginConfiguration {
}
@Bean
- public AiProxyConfigService aiProxyConfigService() {
- return new AiProxyConfigService();
+ public AiProxyConfigService aiProxyConfigService(final
AiProxyPluginHandler aiProxyPluginHandler) {
+ return new AiProxyConfigService(aiProxyPluginHandler);
}
/**
diff --git
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/test/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfigurationTest.java
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/test/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfigurationTest.java
index 8ba08de911..db54707ecf 100644
---
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/test/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfigurationTest.java
+++
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-plugin/shenyu-spring-boot-starter-plugin-ai-proxy/src/test/java/org/apache/shenyu/springboot/starter/plugin/ai/proxy/AiProxyPluginConfigurationTest.java
@@ -19,6 +19,10 @@ package org.apache.shenyu.springboot.starter.plugin.ai.proxy;
import
org.apache.shenyu.plugin.ai.common.spring.ai.registry.AiModelFactoryRegistry;
import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.convert.rule.AiProxyHandle;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.handler.AiProxyPluginHandler;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.service.AiProxyConfigService;
import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProvider;
import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAiCompatible;
@@ -154,4 +158,20 @@ class AiProxyPluginConfigurationTest {
.withPropertyValues("shenyu.plugins.ai.proxy.transport.max-response-bytes=-1")
.run(context ->
org.assertj.core.api.Assertions.assertThat(context).hasFailed());
}
+
+ @Test
+ void resolvesPluginUpdatesFromTheAutoConfiguredHandler() {
+ contextRunner.run(context -> {
+ PluginData plugin = new PluginData();
+
plugin.setConfig("{\"provider\":\"OpenAI\",\"baseUrl\":\"https://example.org\",\"apiKey\":\"global-key\",\"model\":\"global-model\"}");
+ AiProxyPluginHandler handler =
context.getBean(AiProxyPluginHandler.class);
+ handler.handlerPlugin(plugin);
+ AiCommonConfig resolved =
context.getBean(AiProxyConfigService.class).resolvePrimaryConfig(new
AiProxyHandle());
+ assertEquals("global-model", resolved.getModel());
+ assertEquals("global-key", resolved.getApiKey());
+ assertEquals("https://example.org", resolved.getBaseUrl());
+ handler.removePlugin(plugin);
+
org.junit.jupiter.api.Assertions.assertNull(context.getBean(AiProxyConfigService.class).resolvePrimaryConfig(null).getModel());
+ });
+ }
}