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());
+        });
+    }
 }

Reply via email to