Copilot commented on code in PR #6248:
URL: https://github.com/apache/shenyu/pull/6248#discussion_r2587881939


##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -89,25 +89,30 @@ protected Mono<Void> doExecute(
             final ShenyuPluginChain chain,
             final SelectorData selector,
             final RuleData rule) {
-        final AiProxyHandle selectorHandle =
-                aiProxyPluginHandler
-                        .getSelectorCachedHandle()
-                        .obtainHandle(
-                                CacheKeyUtils.INST.getKey(
-                                        selector.getId(), 
Constants.DEFAULT_RULE));
+        final AiProxyHandle selectorHandle = aiProxyPluginHandler
+                .getSelectorCachedHandle()
+                .obtainHandle(
+                        CacheKeyUtils.INST.getKey(
+                                selector.getId(), Constants.DEFAULT_RULE));
+
+        long contentLength = 
exchange.getRequest().getHeaders().getContentLength();
+        if (contentLength > 5 * 1024 * 1024) {

Review Comment:
   The 5MB size limit is hardcoded with a magic number. This should be 
extracted to a named constant (e.g., `MAX_REQUEST_BODY_SIZE_BYTES`) or made 
configurable through plugin settings to allow different limits per deployment 
or selector.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -147,22 +153,22 @@ private Mono<Void> handleStreamRequest(
             final AiCommonConfig primaryConfig,
             final AiProxyHandle selectorHandle) {
         final ChatClient mainClient = createMainChatClient(selector.getId(), 
primaryConfig);
-        final Optional<ChatClient> fallbackClient =
-                resolveFallbackClient(primaryConfig, selectorHandle, 
selector.getId(), requestBody);
+        final Optional<ChatClient> fallbackClient = 
resolveFallbackClient(primaryConfig, selectorHandle,
+                selector.getId(), requestBody);
+        final String prompt = aiProxyConfigService.extractPrompt(requestBody);
         final ServerHttpResponse response = exchange.getResponse();
         response.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM);
 
-        final Flux<ChatResponse> chatResponseFlux =
-                aiProxyExecutorService.executeStream(mainClient, 
fallbackClient, requestBody);
+        final Flux<ChatResponse> chatResponseFlux = 
aiProxyExecutorService.executeStream(mainClient, fallbackClient,
+                prompt);

Review Comment:
   The prompt extraction happens after fallback client resolution, but the 
fallback client resolution also needs the requestBody (line 156-157). This 
creates a potential inconsistency: if the request contains a fallbackConfig, 
the fallback client is resolved using the full requestBody, but the AI call is 
made with the extracted prompt. This means the fallback configuration and the 
actual prompt being sent might be from different parts of the request 
structure. Consider whether the prompt should be extracted before or whether 
both the main and fallback calls should use the same input.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigService.java:
##########
@@ -121,4 +124,30 @@ private Optional<AiCommonConfig> 
extractDynamicFallbackConfig(final String reque
         }
         return Optional.empty();
     }
+
+    /**
+     * Extract prompt from request body if it contains fallback config.
+     *
+     * @param requestBody the request body
+     * @return the extracted prompt or original body
+     */
+    public String extractPrompt(final String requestBody) {
+        if (Objects.isNull(requestBody) || requestBody.isEmpty()) {
+            return requestBody;
+        }
+        try {
+            JsonNode jsonNode = JsonUtils.toJsonNode(requestBody);
+            if (jsonNode.has(FALLBACK_CONFIG)) {
+                if (jsonNode.has("prompt")) {
+                    return jsonNode.get("prompt").asText();
+                }
+                if (jsonNode.has("content")) {
+                    return jsonNode.get("content").asText();
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return requestBody;
+    }

Review Comment:
   The new `extractPrompt` method lacks test coverage. Given that this method 
involves JSON parsing with exception handling and conditional logic, tests 
should be added to cover scenarios: (1) null/empty input, (2) valid JSON with 
fallbackConfig and prompt, (3) valid JSON with fallbackConfig and content, (4) 
valid JSON with fallbackConfig but neither prompt nor content, (5) valid JSON 
without fallbackConfig, and (6) malformed JSON.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -174,15 +180,15 @@ private Mono<Void> handleNonStreamRequest(
             final AiCommonConfig primaryConfig,
             final AiProxyHandle selectorHandle) {
         final ChatClient mainClient = createMainChatClient(selector.getId(), 
primaryConfig);
-        final Optional<ChatClient> fallbackClient =
-                resolveFallbackClient(primaryConfig, selectorHandle, 
selector.getId(), requestBody);
+        final Optional<ChatClient> fallbackClient = 
resolveFallbackClient(primaryConfig, selectorHandle,
+                selector.getId(), requestBody);
+        final String prompt = aiProxyConfigService.extractPrompt(requestBody);
 
         return aiProxyExecutorService
-                .execute(mainClient, fallbackClient, requestBody)
+                .execute(mainClient, fallbackClient, prompt)

Review Comment:
   Same issue as the stream request handler: the prompt is extracted from 
requestBody after fallback client resolution, creating potential inconsistency 
between the fallback configuration source and the prompt being executed.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/cache/ChatClientCache.java:
##########
@@ -30,30 +30,37 @@
  * This is ChatClient cache.
  */
 public final class ChatClientCache {
-
+    
     private static final Logger LOG = 
LoggerFactory.getLogger(ChatClientCache.class);
-
+    
+    private static final int MAX_CACHE_SIZE = 500;

Review Comment:
   The MAX_CACHE_SIZE constant is hardcoded to 500 with no configuration 
option. This should be externalized to a configuration property to allow 
administrators to tune cache size based on their deployment needs and memory 
constraints.
   ```suggestion
       private static final int MAX_CACHE_SIZE = getCacheSize();
   
       private static int getCacheSize() {
           String value = 
System.getProperty("shenyu.plugin.ai.proxy.enhanced.cache.maxSize",
                   
System.getenv("SHENYU_PLUGIN_AI_PROXY_ENHANCED_CACHE_MAXSIZE"));
           if (value != null) {
               try {
                   return Integer.parseInt(value);
               } catch (NumberFormatException e) {
                   LoggerFactory.getLogger(ChatClientCache.class)
                           .warn("[ChatClientCache] Invalid cache size '{}', 
using default 500.", value);
               }
           }
           return 500;
       }
   ```



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/cache/ChatClientCache.java:
##########
@@ -30,30 +30,37 @@
  * This is ChatClient cache.
  */
 public final class ChatClientCache {
-
+    
     private static final Logger LOG = 
LoggerFactory.getLogger(ChatClientCache.class);
-
+    
+    private static final int MAX_CACHE_SIZE = 500;
+    
     private final Map<String, ChatClient> chatClientMap = new 
ConcurrentHashMap<>();
-
+    
     /**
      * Instantiates a new Chat client cache.
      */
     public ChatClientCache() {
     }
-
+    
     /**
      * Gets client or compute if absent.
      *
-     * @param key               the key
+     * @param key the key
      * @param chatModelSupplier the chat model supplier
      * @return the chat client
      */
     public ChatClient computeIfAbsent(final String key, final 
Supplier<ChatModel> chatModelSupplier) {
+        if (chatClientMap.size() > MAX_CACHE_SIZE) {
+            LOG.warn("[ChatClientCache] Cache size exceeded {}, clearing 
all.", MAX_CACHE_SIZE);
+            chatClientMap.clear();
+        }

Review Comment:
   The cache eviction strategy has a critical race condition. Multiple 
concurrent requests could check the size condition (line 54) simultaneously 
before any clear operation completes, leading to unpredictable cache state. 
Additionally, clearing the entire cache when the size limit is exceeded is too 
aggressive and will cause a thundering herd problem where all subsequent 
requests will need to recreate their clients simultaneously. Consider using a 
proper LRU cache implementation or at least implement proper synchronization 
and evict only the oldest entries.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -89,25 +89,30 @@ protected Mono<Void> doExecute(
             final ShenyuPluginChain chain,
             final SelectorData selector,
             final RuleData rule) {
-        final AiProxyHandle selectorHandle =
-                aiProxyPluginHandler
-                        .getSelectorCachedHandle()
-                        .obtainHandle(
-                                CacheKeyUtils.INST.getKey(
-                                        selector.getId(), 
Constants.DEFAULT_RULE));
+        final AiProxyHandle selectorHandle = aiProxyPluginHandler
+                .getSelectorCachedHandle()
+                .obtainHandle(
+                        CacheKeyUtils.INST.getKey(
+                                selector.getId(), Constants.DEFAULT_RULE));
+
+        long contentLength = 
exchange.getRequest().getHeaders().getContentLength();
+        if (contentLength > 5 * 1024 * 1024) {
+            exchange.getResponse().setStatusCode(HttpStatus.PAYLOAD_TOO_LARGE);
+            return exchange.getResponse().setComplete();
+        }

Review Comment:
   The request body size limit check only validates the Content-Length header, 
which can be spoofed or missing. An attacker could send a request without a 
Content-Length header or with a false value to bypass this check. The actual 
body size should be validated after reading it at line 106, or a configurable 
reactive body size limit should be enforced at the gateway level.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyConfigService.java:
##########
@@ -121,4 +124,30 @@ private Optional<AiCommonConfig> 
extractDynamicFallbackConfig(final String reque
         }
         return Optional.empty();
     }
+
+    /**
+     * Extract prompt from request body if it contains fallback config.
+     *
+     * @param requestBody the request body
+     * @return the extracted prompt or original body
+     */
+    public String extractPrompt(final String requestBody) {
+        if (Objects.isNull(requestBody) || requestBody.isEmpty()) {
+            return requestBody;
+        }
+        try {
+            JsonNode jsonNode = JsonUtils.toJsonNode(requestBody);
+            if (jsonNode.has(FALLBACK_CONFIG)) {
+                if (jsonNode.has("prompt")) {
+                    return jsonNode.get("prompt").asText();
+                }
+                if (jsonNode.has("content")) {
+                    return jsonNode.get("content").asText();
+                }
+            }

Review Comment:
   The extractPrompt method has incorrect logic. It only extracts "prompt" or 
"content" fields when a "fallbackConfig" field exists in the JSON, but this 
doesn't make semantic sense. The presence of fallbackConfig shouldn't determine 
whether to extract the prompt. The method should either always attempt to 
extract the prompt from the standard fields, or the logic should be clarified 
with proper documentation explaining why prompt extraction depends on 
fallbackConfig presence.
   ```suggestion
               if (jsonNode.has("prompt")) {
                   return jsonNode.get("prompt").asText();
               }
               if (jsonNode.has("content")) {
                   return jsonNode.get("content").asText();
               }
   ```



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -202,36 +208,36 @@ private Optional<ChatClient> resolveFallbackClient(
                     return createDynamicFallbackClient(cfg);
                 })
                 .or(
-                        () ->
-                                aiProxyConfigService
-                                        
.resolveAdminFallbackConfig(primaryConfig, selectorHandle)
-                                        .map(adminFallbackConfig -> {
-                                            LOG.info("[AiProxy] use admin 
fallback");
-                                            if (LOG.isDebugEnabled()) {
-                                                LOG.debug("[AiProxy] admin 
fallback config: {}", adminFallbackConfig);
-                                            }
-                                            return 
createAdminFallbackClient(selectorId, adminFallbackConfig);
-                                        }));
+                        () -> aiProxyConfigService
+                                .resolveAdminFallbackConfig(primaryConfig, 
selectorHandle)
+                                .map(adminFallbackConfig -> {
+                                    LOG.info("[AiProxy] use admin fallback");
+                                    if (LOG.isDebugEnabled()) {
+                                        LOG.debug("[AiProxy] admin fallback 
config: {}", adminFallbackConfig);
+                                    }
+                                    return 
createAdminFallbackClient(selectorId, adminFallbackConfig);
+                                }));
     }
 
     private ChatClient createMainChatClient(final String selectorId, final 
AiCommonConfig config) {
+        final String cacheKey = selectorId + "_" + config.hashCode();

Review Comment:
   Cache invalidation is broken due to key format mismatch. The main client 
cache key uses underscore separator (e.g., "selectorId_hashcode") at line 223, 
but the ChatClientCache.remove() method at line 72 only removes keys that 
either equal the selectorId exactly or start with "selectorId|". This means 
main client cache entries will never be properly invalidated when the selector 
is updated or removed, leading to memory leaks and stale client usage.
   ```suggestion
           final String cacheKey = selectorId + "|" + config.hashCode();
   ```



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -202,36 +208,36 @@ private Optional<ChatClient> resolveFallbackClient(
                     return createDynamicFallbackClient(cfg);
                 })
                 .or(
-                        () ->
-                                aiProxyConfigService
-                                        
.resolveAdminFallbackConfig(primaryConfig, selectorHandle)
-                                        .map(adminFallbackConfig -> {
-                                            LOG.info("[AiProxy] use admin 
fallback");
-                                            if (LOG.isDebugEnabled()) {
-                                                LOG.debug("[AiProxy] admin 
fallback config: {}", adminFallbackConfig);
-                                            }
-                                            return 
createAdminFallbackClient(selectorId, adminFallbackConfig);
-                                        }));
+                        () -> aiProxyConfigService
+                                .resolveAdminFallbackConfig(primaryConfig, 
selectorHandle)
+                                .map(adminFallbackConfig -> {
+                                    LOG.info("[AiProxy] use admin fallback");
+                                    if (LOG.isDebugEnabled()) {
+                                        LOG.debug("[AiProxy] admin fallback 
config: {}", adminFallbackConfig);
+                                    }
+                                    return 
createAdminFallbackClient(selectorId, adminFallbackConfig);
+                                }));
     }
 
     private ChatClient createMainChatClient(final String selectorId, final 
AiCommonConfig config) {
+        final String cacheKey = selectorId + "_" + config.hashCode();

Review Comment:
   Using `config.hashCode()` in the cache key is problematic because it 
includes the `apiKey` field in the hash calculation (as seen in 
AiCommonConfig.hashCode()). When the apiKey is updated at runtime (e.g., via 
proxy key replacement at line 127), the same config object will produce a 
different hashCode, causing cache misses for what should be logically the same 
configuration. This breaks the caching mechanism and can lead to resource leaks 
as old cache entries are never removed. Consider creating a cache key based 
only on immutable config fields (provider, model, baseUrl, temperature, 
maxTokens, stream) or implement a proper config identifier.



##########
shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy-enhanced/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java:
##########
@@ -202,36 +208,36 @@ private Optional<ChatClient> resolveFallbackClient(
                     return createDynamicFallbackClient(cfg);
                 })
                 .or(
-                        () ->
-                                aiProxyConfigService
-                                        
.resolveAdminFallbackConfig(primaryConfig, selectorHandle)
-                                        .map(adminFallbackConfig -> {
-                                            LOG.info("[AiProxy] use admin 
fallback");
-                                            if (LOG.isDebugEnabled()) {
-                                                LOG.debug("[AiProxy] admin 
fallback config: {}", adminFallbackConfig);
-                                            }
-                                            return 
createAdminFallbackClient(selectorId, adminFallbackConfig);
-                                        }));
+                        () -> aiProxyConfigService
+                                .resolveAdminFallbackConfig(primaryConfig, 
selectorHandle)
+                                .map(adminFallbackConfig -> {
+                                    LOG.info("[AiProxy] use admin fallback");
+                                    if (LOG.isDebugEnabled()) {
+                                        LOG.debug("[AiProxy] admin fallback 
config: {}", adminFallbackConfig);
+                                    }
+                                    return 
createAdminFallbackClient(selectorId, adminFallbackConfig);
+                                }));
     }
 
     private ChatClient createMainChatClient(final String selectorId, final 
AiCommonConfig config) {
+        final String cacheKey = selectorId + "_" + config.hashCode();
         return chatClientCache.computeIfAbsent(
-                selectorId,
+                cacheKey,
                 () -> {
-                    LOG.info("Creating and caching main model for selector: 
{}", selectorId);
+                    LOG.info("Creating and caching main model for selector: 
{}, key: {}", selectorId, cacheKey);
                     return createChatModel(config);
                 });
     }
 
     private ChatClient createAdminFallbackClient(
             final String selectorId, final AiCommonConfig fallbackConfig) {
-        final String fallbackCacheKey = selectorId + "|adminFallback";
+        final String fallbackCacheKey = selectorId + "|adminFallback_" + 
fallbackConfig.hashCode();

Review Comment:
   The same hashCode issue applies here. Using `fallbackConfig.hashCode()` in 
the cache key will cause cache misses when configuration changes, and the cache 
invalidation logic at ChatClientCache.remove() won't properly clean up these 
entries because the removal logic only matches prefixes starting with 
selectorId (see ChatClientCache line 72), but these keys now use a different 
separator ("_" for main and "|adminFallback_" for fallback).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to