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 143070de07 refactor(ai-proxy): separate response adapter from upstream 
request (#7876)
143070de07 is described below

commit 143070de074ca522c663900b9719348deec13f72
Author: hengyuss <[email protected]>
AuthorDate: Wed Oct 21 10:31:04 2026 +0800

    refactor(ai-proxy): separate response adapter from upstream request (#7876)
---
 .../enhanced/conversion/AiResponseAdapter.java     | 45 +++++++++++
 .../ai/proxy/enhanced/model/AiUpstreamRequest.java | 22 -----
 .../ai/proxy/enhanced/model/PreparedAiRequest.java | 38 +++++++++
 .../proxy/enhanced/provider/AiProxyProvider.java   | 14 ++++
 .../plugin/ai/proxy/enhanced/provider/OpenAi.java  | 12 ++-
 .../proxy/enhanced/provider/OpenAiCompatible.java  |  6 ++
 .../ai/proxy/enhanced/service/AiProxyEngine.java   | 19 +++--
 .../provider/AiProxyProviderFactoryTest.java       | 28 +++++++
 .../provider/OpenAiProviderOptionsTest.java        |  8 +-
 .../service/AiProxyEngineOrchestrationTest.java    | 93 +++++++++++++++-------
 10 files changed, 224 insertions(+), 61 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/conversion/AiResponseAdapter.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/conversion/AiResponseAdapter.java
new file mode 100644
index 0000000000..7cd61367ae
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/conversion/AiResponseAdapter.java
@@ -0,0 +1,45 @@
+/*
+ * 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.enhanced.conversion;
+
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+
+/**
+ * Request-scoped adapter for converting an upstream response.
+ */
+@FunctionalInterface
+public interface AiResponseAdapter {
+
+    /**
+     * Adapt an upstream response.
+     *
+     * @param response upstream response
+     * @return adapted response
+     */
+    AiUpstreamResponse adapt(AiUpstreamResponse response);
+
+    /**
+     * Return an adapter that leaves the response unchanged.
+     *
+     * @return identity response adapter
+     */
+    static AiResponseAdapter identity() {
+        return response -> response;
+    }
+
+}
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/AiUpstreamRequest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/AiUpstreamRequest.java
index 11ea070761..8be38557b2 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/AiUpstreamRequest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/AiUpstreamRequest.java
@@ -20,7 +20,6 @@ package org.apache.shenyu.plugin.ai.proxy.enhanced.model;
 import java.net.URI;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
 
 /**
  * Protocol-independent request sent to an AI upstream.
@@ -35,27 +34,6 @@ public final class AiUpstreamRequest {
 
     private byte[] body;
 
-    private Function<AiUpstreamResponse, AiUpstreamResponse> responseAdapter = 
Function.identity();
-
-    /**
-     * Bind request-scoped protocol conversion to its corresponding response.
-     *
-     * @param responseAdapter response conversion retaining this request's 
tool context
-     */
-    public void setResponseAdapter(final Function<AiUpstreamResponse, 
AiUpstreamResponse> responseAdapter) {
-        this.responseAdapter = 
java.util.Objects.requireNonNull(responseAdapter);
-    }
-
-    /**
-     * Adapt the upstream response without subscribing to its body.
-     *
-     * @param response provider response
-     * @return client-protocol response
-     */
-    public AiUpstreamResponse adaptResponse(final AiUpstreamResponse response) 
{
-        return responseAdapter.apply(response);
-    }
-
     /**
      * Get request URI.
      *
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/PreparedAiRequest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/PreparedAiRequest.java
new file mode 100644
index 0000000000..169411c21f
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/PreparedAiRequest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.enhanced.model;
+
+import org.apache.shenyu.plugin.ai.proxy.enhanced.conversion.AiResponseAdapter;
+
+import java.util.Objects;
+
+/**
+ * Prepared upstream request and its request-scoped response adapter.
+ *
+ * @param request upstream request
+ * @param responseAdapter response adapter bound while preparing the request
+ */
+public record PreparedAiRequest(
+        AiUpstreamRequest request,
+        AiResponseAdapter responseAdapter) {
+
+    public PreparedAiRequest {
+        Objects.requireNonNull(request, "AI upstream request is required");
+        Objects.requireNonNull(responseAdapter, "AI response adapter is 
required");
+    }
+}
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProvider.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProvider.java
index aff2bed098..5bec34746f 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProvider.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProvider.java
@@ -18,8 +18,10 @@
 package org.apache.shenyu.plugin.ai.proxy.enhanced.provider;
 
 import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.conversion.AiResponseAdapter;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 
 import java.util.Objects;
@@ -74,4 +76,16 @@ public interface AiProxyProvider {
     default AiUpstreamResponse adaptResponse(final AiUpstreamResponse 
response) {
         return response;
     }
+
+    /**
+     * Prepare an upstream request and the response conversion bound to it.
+     * Existing providers receive an identity response adapter by default.
+     *
+     * @param request decoded AI request
+     * @param config provider configuration
+     * @return prepared upstream request
+     */
+    default PreparedAiRequest prepareRequest(final ShenyuAiRequest request, 
final AiCommonConfig config) {
+        return new PreparedAiRequest(buildRequest(request, config), 
AiResponseAdapter.identity());
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAi.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAi.java
index ceedf9db2f..ac8e52cb2d 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAi.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAi.java
@@ -22,8 +22,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import io.github.agenticai.connect.ProviderProfile;
 import org.apache.shenyu.common.exception.ShenyuException;
 import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.conversion.AiResponseAdapter;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.ConnectProtocolAdapter;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiResponses;
@@ -99,6 +101,11 @@ public final class OpenAi implements AiProxyProvider {
 
     @Override
     public AiUpstreamRequest buildRequest(final ShenyuAiRequest request, final 
AiCommonConfig config) {
+        return prepareRequest(request, config).request();
+    }
+
+    @Override
+    public PreparedAiRequest prepareRequest(final ShenyuAiRequest request, 
final AiCommonConfig config) {
         validateConfig(config);
         if (!supports(request.getProtocol())) {
             throw new IllegalArgumentException("Unsupported OpenAI protocol: " 
+ request.getProtocol());
@@ -155,8 +162,9 @@ public final class OpenAi implements AiProxyProvider {
         upstream.setHeaders(headers(config, stream));
         String body = request.getRawPayload().equals(converted) ? 
request.getRawBody() : converted.toString();
         upstream.setBody(body.getBytes(StandardCharsets.UTF_8));
-        upstream.setResponseAdapter(response -> 
adapter.convertResponse(response, stream));
-        return upstream;
+        AiResponseAdapter responseAdapter =
+                response -> adapter.convertResponse(response, stream);
+        return new PreparedAiRequest(upstream, responseAdapter);
     }
 
     private Map<String, List<String>> headers(final AiCommonConfig config, 
final boolean stream) {
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiCompatible.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiCompatible.java
index e86964630c..79ad7a0974 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiCompatible.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiCompatible.java
@@ -19,6 +19,7 @@ package org.apache.shenyu.plugin.ai.proxy.enhanced.provider;
 
 import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 
 import java.util.Objects;
@@ -69,4 +70,9 @@ public final class OpenAiCompatible implements 
AiProxyProvider {
     public AiUpstreamRequest buildRequest(final ShenyuAiRequest request, final 
AiCommonConfig config) {
         return delegate.buildRequest(request, config);
     }
+
+    @Override
+    public PreparedAiRequest prepareRequest(final ShenyuAiRequest request, 
final AiCommonConfig config) {
+        return delegate.prepareRequest(request, config);
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngine.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngine.java
index 088fb9156e..401c2b2f80 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngine.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngine.java
@@ -24,9 +24,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.shenyu.common.exception.ShenyuException;
 import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.conversion.AiResponseAdapter;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocol;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocolFactory;
@@ -136,8 +138,9 @@ public final class AiProxyEngine {
                         }
                         return response.getBody().then(Mono.error(new 
UpstreamStatusFailure(status)));
                     }
-                    AiUpstreamResponse adapted = 
prepared.request().adaptResponse(prepared.provider().adaptResponse(response));
-                    AiUpstreamResponse encoded = 
prepared.protocol().encodeResponse(adapted, prepared.stream());
+                    AiUpstreamResponse providerAdapted = 
prepared.provider().adaptResponse(response);
+                    AiUpstreamResponse requestAdapted = 
prepared.responseAdapter().adapt(providerAdapted);
+                    AiUpstreamResponse encoded = 
prepared.protocol().encodeResponse(requestAdapted, prepared.stream());
                     return consumeResponse(encoded, consumer, emitted);
                 })
                 .onErrorResume(error -> {
@@ -230,14 +233,20 @@ public final class AiProxyEngine {
         boolean stream = decoded.hasStream() ? decoded.isStream() : 
Boolean.TRUE.equals(config.getStream());
         provider.capabilities().validate(decoded, stream);
         try {
-            AiUpstreamRequest request = provider.buildRequest(decoded, config);
-            return new PreparedExchange(protocol, provider, request, stream);
+            PreparedAiRequest preparedRequest = 
provider.prepareRequest(decoded, config);
+            return new PreparedExchange(protocol, provider, 
preparedRequest.request(),
+                    preparedRequest.responseAdapter(), stream);
         } catch (IllegalArgumentException error) {
             throw new AiProxyException(AiProxyException.Kind.PROVIDER, 
"Invalid AI provider configuration", error);
         }
     }
 
-    private record PreparedExchange(AiProxyProtocol protocol, AiProxyProvider 
provider, AiUpstreamRequest request, boolean stream) {
+    private record PreparedExchange(
+            AiProxyProtocol protocol,
+            AiProxyProvider provider,
+            AiUpstreamRequest request,
+            AiResponseAdapter responseAdapter,
+            boolean stream) {
     }
 
     private static final class UpstreamStatusFailure extends RuntimeException {
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderFactoryTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderFactoryTest.java
index 9225b5abb3..f6dea7204f 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderFactoryTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderFactoryTest.java
@@ -18,9 +18,16 @@
 package org.apache.shenyu.plugin.ai.proxy.enhanced.provider;
 
 import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
 
 import java.util.List;
+import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -45,4 +52,25 @@ class AiProxyProviderFactoryTest {
         assertThrows(ShenyuException.class, () -> 
factory.getProvider("unknown"));
         assertThrows(ShenyuException.class, () -> factory.getProvider(null));
     }
+
+    @Test
+    void testExistingProviderReceivesIdentityResponseAdapter() {
+        final AiUpstreamRequest upstreamRequest = new AiUpstreamRequest();
+        final AiProxyProvider provider = new AiProxyProvider() {
+            @Override
+            public String name() {
+                return "legacy";
+            }
+
+            @Override
+            public AiUpstreamRequest buildRequest(final ShenyuAiRequest 
request, final AiCommonConfig config) {
+                return upstreamRequest;
+            }
+        };
+        final PreparedAiRequest prepared = provider.prepareRequest(new 
ShenyuAiRequest(), new AiCommonConfig());
+        final AiUpstreamResponse response = new AiUpstreamResponse(200, 
Map.of(), Flux.empty());
+
+        assertSame(upstreamRequest, prepared.request());
+        assertSame(response, prepared.responseAdapter().adapt(response));
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptionsTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptionsTest.java
index 6b349ed8d9..0800e25581 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptionsTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptionsTest.java
@@ -24,6 +24,7 @@ import 
org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocolFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiChat;
@@ -234,9 +235,10 @@ class OpenAiProviderOptionsTest {
                 + 
"\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"reasoning_content\":\"think\",\"content\":\"answer\"},\"finish_reason\":\"stop\"}],"
                 + 
"\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":5,\"total_tokens\":8,\"prompt_cache_hit_tokens\":2}}";
         AiUpstreamResponse response = new AiUpstreamResponse(200, 
Map.of("Content-Type", List.of("application/json")), Flux.just(bytes(reply)));
-        assertEquals(reply, text(provider.buildRequest(chat(CHAT), 
config).adaptResponse(response).getBody().single().block(Duration.ofSeconds(3))));
-        AiUpstreamRequest request = 
provider.buildRequest(responses("{\"model\":\"deepseek-flash\",\"input\":\"hi\"}"),
 config);
-        JsonNode converted = 
MAPPER.readTree(request.adaptResponse(response).getBody().single().block(Duration.ofSeconds(3)));
+        PreparedAiRequest chat = provider.prepareRequest(chat(CHAT), config);
+        assertEquals(reply, 
text(chat.responseAdapter().adapt(response).getBody().single().block(Duration.ofSeconds(3))));
+        PreparedAiRequest responses = 
provider.prepareRequest(responses("{\"model\":\"deepseek-flash\",\"input\":\"hi\"}"),
 config);
+        JsonNode converted = 
MAPPER.readTree(responses.responseAdapter().adapt(response).getBody().single().block(Duration.ofSeconds(3)));
         assertEquals("response", converted.get("object").asText());
         assertTrue(converted.toString().contains("think"));
         assertTrue(converted.toString().contains("answer"));
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineOrchestrationTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineOrchestrationTest.java
index ee485ee542..fe77d269f7 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineOrchestrationTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineOrchestrationTest.java
@@ -17,16 +17,19 @@
 
 package org.apache.shenyu.plugin.ai.proxy.enhanced.service;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.shenyu.common.exception.ShenyuException;
 import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.PreparedAiRequest;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocol;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocolFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiChat;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiResponses;
-import 
org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProviderCapabilities;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProvider;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProviderFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAi;
@@ -55,8 +58,6 @@ import static 
org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 /**
  * Retry, fallback, response adaptation and cancellation across the engine 
boundary.
@@ -416,38 +417,72 @@ class AiProxyEngineOrchestrationTest {
     }
 
     @Test
-    void providerAndProtocolBothAdaptTheResponse() {
-        AtomicBoolean providerAdapted = new AtomicBoolean();
-        AtomicBoolean protocolEncoded = new AtomicBoolean();
-        AiProxyProvider provider = mock(AiProxyProvider.class);
-        when(provider.name()).thenReturn("custom");
-        when(provider.supports("openai-chat")).thenReturn(true);
-        
when(provider.capabilities()).thenReturn(AiProviderCapabilities.chat());
-        when(provider.buildRequest(org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any())).thenReturn(new AiUpstreamRequest());
-        
when(provider.adaptResponse(org.mockito.ArgumentMatchers.any())).thenAnswer(invocation
 -> {
-            providerAdapted.set(true);
-            return invocation.getArgument(0);
-        });
-        AiProxyProtocol protocol = mock(AiProxyProtocol.class);
-        when(protocol.name()).thenReturn("openai-chat");
-        
when(protocol.matches(org.mockito.ArgumentMatchers.any())).thenReturn(true);
-        when(protocol.decodeRequest(org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any())).thenAnswer(invocation ->
-                new OpenAiChat().decodeRequest(invocation.getArgument(0), 
invocation.getArgument(1)));
-        when(protocol.encodeResponse(org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.anyBoolean())).thenAnswer(invocation -> {
-            assertTrue(providerAdapted.get());
-            protocolEncoded.set(true);
-            return invocation.getArgument(0);
-        });
+    void providerRequestAdapterAndProtocolConvertTheResponseInOrder() {
+        List<String> conversions = new ArrayList<>();
+        AtomicBoolean supported = new AtomicBoolean(true);
+        AiProxyProvider provider = new AiProxyProvider() {
+            @Override
+            public String name() {
+                return "custom";
+            }
+
+            @Override
+            public boolean supports(final String protocol) {
+                return supported.get();
+            }
+
+            @Override
+            public AiUpstreamRequest buildRequest(final ShenyuAiRequest 
request, final AiCommonConfig config) {
+                return new AiUpstreamRequest();
+            }
+
+            @Override
+            public PreparedAiRequest prepareRequest(final ShenyuAiRequest 
request, final AiCommonConfig config) {
+                return new PreparedAiRequest(buildRequest(request, config), 
response -> {
+                    conversions.add("request");
+                    return response;
+                });
+            }
+
+            @Override
+            public AiUpstreamResponse adaptResponse(final AiUpstreamResponse 
response) {
+                conversions.add("provider");
+                return response;
+            }
+        };
+        OpenAiChat delegate = new OpenAiChat();
+        AiProxyProtocol protocol = new AiProxyProtocol() {
+            @Override
+            public String name() {
+                return delegate.name();
+            }
+
+            @Override
+            public boolean matches(final JsonNode payload) {
+                return delegate.matches(payload);
+            }
+
+            @Override
+            public ShenyuAiRequest decodeRequest(final String rawBody, final 
ObjectNode payload) {
+                return delegate.decodeRequest(rawBody, payload);
+            }
+
+            @Override
+            public AiUpstreamResponse encodeResponse(final AiUpstreamResponse 
response, final boolean stream) {
+                conversions.add("protocol");
+                return response;
+            }
+        };
         AiCommonConfig config = config(false, "model");
         config.setProvider("custom");
         AiProxyEngine engine = new AiProxyEngine(new 
AiProxyProtocolFactory(List.of(protocol)), new 
AiProxyProviderFactory(List.of(provider)),
                 request -> Mono.just(response(200, Flux.just(bytes("ok")))), 
Duration.ZERO);
         StepVerifier.create(engine.execute(BODY, config, Optional.empty(), 
response -> response.getBody().then())).verifyComplete();
-        assertTrue(protocolEncoded.get());
-        when(provider.supports("openai-chat")).thenReturn(false);
-        providerAdapted.set(false);
+        assertEquals(List.of("provider", "request", "protocol"), conversions);
+        supported.set(false);
+        conversions.clear();
         StepVerifier.create(engine.execute(BODY, config, Optional.empty(), 
response -> 
response.getBody().then())).expectError(ShenyuException.class).verify();
-        assertFalse(providerAdapted.get());
+        assertTrue(conversions.isEmpty());
     }
 
     @Test

Reply via email to