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 99caf99584 refactor(ai-proxy): integrate Connect protocol conversion 
(#7575)
99caf99584 is described below

commit 99caf99584e0bbc85e1948cd28c48f997be4cd5e
Author: shown <[email protected]>
AuthorDate: Tue Oct 13 09:55:28 2026 +0800

    refactor(ai-proxy): integrate Connect protocol conversion (#7575)
    
    * refactor(ai-proxy): integrate Connect protocol conversion
    
    * docs(ai-proxy): document Connect profile and protocol entry points
---
 .../src/main/release-docs/LICENSE                  |   1 +
 .../main/release-docs/licenses/LICENSE-connect.txt |  21 ++
 .../shenyu-plugin-ai-proxy/README.md               | 124 ++++++++++
 .../shenyu-plugin-ai-proxy/pom.xml                 |   7 +
 .../ai/proxy/enhanced/model/AiUpstreamRequest.java |  22 ++
 .../enhanced/protocol/ConnectProtocolAdapter.java  | 189 +++++++++++++++
 .../proxy/enhanced/protocol/OpenAiResponses.java   |  75 ++++++
 .../plugin/ai/proxy/enhanced/provider/OpenAi.java  |  46 +++-
 .../proxy/enhanced/provider/OpenAiCompatible.java  |   5 +-
 .../ai/proxy/enhanced/service/AiProxyEngine.java   |   4 +-
 .../enhanced/AiProxyHttpCompatibilityTest.java     | 107 ++++++++-
 .../protocol/ConnectProtocolAdapterTest.java       | 258 +++++++++++++++++++++
 .../enhanced/protocol/OpenAiResponsesTest.java     |  82 +++++++
 .../service/AiProxyEngineOrchestrationTest.java    |  48 +++-
 .../ai/proxy/AiProxyPluginConfiguration.java       |  11 +
 .../ai/proxy/AiProxyPluginConfigurationTest.java   |   4 +
 16 files changed, 990 insertions(+), 14 deletions(-)

diff --git a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE 
b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
index 6c018b594f..6e66f46a10 100644
--- a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
+++ b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
@@ -726,6 +726,7 @@ MIT licenses
 The following components are provided under the MIT License. See project link 
for details.
 The text of each license is also included at licenses/LICENSE-[project].txt.
 
+    connect 0.1.0: https://github.com/agentic-ai-java/connect, MIT (see 
licenses/LICENSE-connect.txt)
     jtokkit 1.1.0: https://github.com/knuddelsgmbh/jtokkit, MIT
     animal-sniffer-annotations 1.23: 
https://github.com/mojohaus/animal-sniffer, MIT
     bcpkix-jdk15on 1.68: http://www.bouncycastle.org/licence.html , MIT
diff --git 
a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/licenses/LICENSE-connect.txt
 
b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/licenses/LICENSE-connect.txt
new file mode 100644
index 0000000000..301bf95e13
--- /dev/null
+++ 
b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/licenses/LICENSE-connect.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 connect contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
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
new file mode 100644
index 0000000000..eda17a845e
--- /dev/null
+++ b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
@@ -0,0 +1,124 @@
+<!--
+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.
+-->
+
+# AI Proxy protocol conversion with Connect
+
+The AI Proxy uses `io.github.agentic-ai-java:connect:0.1.0` for JSON protocol
+conversion and explicitly selected provider dialects. ShenYu remains the
+reactive gateway, not an agent runtime. This integration supports OpenAI Chat
+Completions and OpenAI Responses clients against existing Chat Completions
+upstreams. It does not add Claude, Gemini, or native Responses upstream 
transports.
+
+## Responsibility boundary
+
+| Concern | Connect | ShenYu |
+| --- | --- | --- |
+| Protocol semantics | Convert request/response JSON, messages, tool 
definitions/calls, reasoning, finish reasons and usage | Validate the HTTP 
request envelope and select the client protocol |
+| Provider dialect | Apply an explicitly selected profile, such as DeepSeek 
Chat | Resolve provider, routed model and gateway parameter overrides |
+| Conversion losses | Produce structured diagnostics and enforce the `SAFE` 
loss policy | Classify failures and log diagnostic codes without request 
content or secrets |
+| Request context | Retain tool-name/type mappings needed for response 
restoration | Allocate one context per request and bind it to the matching 
response |
+| Streaming | Convert JSON chunks into protocol events and finalize conversion 
state | Parse/frame SSE bytes, validate content type, preserve backpressure and 
cancellation, enforce conversion limits |
+| Gateway execution | No networking or gateway policy | Configuration 
synchronization, selector/rule matching, credentials, endpoints, WebClient 
transport, HTTP status/headers, retries and fallback |
+| Billing and agents | Normalize usage; no charging or tool execution | 
Billing/quota policy remains a gateway concern; this change adds no billing 
system or agent/tool execution |
+
+Protocol field mappings belong in Connect, not duplicated in provider classes.
+A new upstream protocol still requires a ShenYu transport/provider integration:
+a library codec alone does not define its endpoint or authentication contract.
+
+## Exchange lifecycle
+
+1. ShenYu resolves global/selector configuration, proxy credentials and 
fallback.
+2. The protocol decoder validates the envelope and retains the original 
payload.
+3. The provider applies gateway overrides, then `ConnectProtocolAdapter` 
converts
+   to Chat using a request-scoped `ConversionContext`. The configured upstream
+   model is authoritative. The provider profile runs once after conversion.
+4. ShenYu constructs the HTTP endpoint and authorization headers and sends the
+   request through its existing reactive transport.
+5. The request-bound response adapter uses the same context to convert 
successful
+   Chat responses back to Responses, including custom-tool restoration. HTTP
+   errors and Chat client responses remain passthrough.
+6. For streams, ShenYu incrementally frames upstream SSE; Connect consumes JSON
+   chunks; ShenYu emits `event: <type>` and `data: <json>` Responses SSE 
frames.
+   Stream state is allocated per subscription, including retries. Finalization
+   runs only on normal completion, never cancellation or transport failure.
+
+## Configuration and use
+
+Add the existing AI Proxy starter to the gateway. Its configuration registers
+both `openai-chat` and `openai-responses` protocol implementations.
+Set the selector handle's existing `protocol` field explicitly for a Responses
+route; the default remains `openai-chat` for compatibility. For example:
+
+```json
+{
+  "provider": "OpenAI",
+  "baseUrl": "https://api.openai.com";,
+  "apiKey": "<upstream-api-key>",
+  "model": "gpt-4o-mini",
+  "protocol": "openai-responses",
+  "stream": false
+}
+```
+
+Route a client request such as the following to that selector:
+
+```json
+{
+  "model": "client-model-alias",
+  "input": "Explain reactive backpressure briefly.",
+  "max_output_tokens": 128,
+  "stream": true
+}
+```
+
+The upstream receives Chat `messages`, the configured routed model and
+`max_completion_tokens` at `/v1/chat/completions`. The client receives 
Responses
+events, including usage when supplied upstream. An explicit client `stream`
+setting takes precedence over the configured default. Existing `maxTokens` and
+`temperature` gateway overrides still apply before conversion.
+
+The existing `DeepSeek` provider identifier selects Connect's DeepSeek Chat
+profile. Other compatible identifiers (`ALiYun`, `OpenAPI`, `Moonshot`) remain
+generic; neither URLs nor model names are used to guess a dialect. DeepSeek's
+profile maps token-limit and reasoning/thinking fields and rejects incompatible
+thinking/tool configurations rather than silently changing their meaning.
+
+## Compatibility and limits
+
+- Existing Chat routes retain their protocol identifier, upstream endpoint,
+  credential handling, response bytes and SSE framing. Requests remain 
byte-for-byte
+  unchanged when no gateway override, stream-usage addition or provider 
adaptation
+  changes the payload; generic extension fields are retained on identity 
conversion.
+- Conversion uses Connect's `SAFE` policy. Error-level losses, including 
Responses
+  conversation state that cannot be represented by Chat, fail with HTTP 400 
before
+  sending a request. Warning-level conversions are permitted and logged by 
code.
+  This is not a stateful Responses API service or hosted-tool implementation.
+- Complete Responses conversion buffers at most 5 MiB. Streaming Responses
+  conversion accepts at most 5 MiB of cumulative framed upstream data because 
the
+  codec retains output/tool state. Existing Chat passthrough has no new 
cumulative
+  response limit. Oversized conversions fail; after commitment the stream 
aborts.
+- Successful conversion removes stale length/digest/ETag headers. Upstream HTTP
+  errors retain their original status/body. Conversion errors are not retried;
+  ShenYu's existing transport/status retry and fallback rules remain in force.
+  No retry or fallback occurs after a converted response event reaches the 
client.
+- Usage conversion is not billing. Provider charges and gateway quotas must not
+  be inferred from synthetic or missing usage values.
+
+No database/configuration migration is required. Existing Chat routes need no
+change. Responses routes opt in through the selector handle; DeepSeek routes
+should review the stricter dialect validation. Roll back by reverting the
+integration commit and restoring any opt-in selectors to `openai-chat`.
diff --git a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/pom.xml 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/pom.xml
index 3adef8c041..74ac1bc6e8 100644
--- a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/pom.xml
+++ b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/pom.xml
@@ -26,6 +26,13 @@
     <artifactId>shenyu-plugin-ai-proxy</artifactId>
 
     <dependencies>
+        <!-- Protocol conversion only; ShenYu retains transport and gateway 
policy. -->
+        <dependency>
+            <groupId>io.github.agentic-ai-java</groupId>
+            <artifactId>connect</artifactId>
+            <version>0.1.0</version>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-plugin-base</artifactId>
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 8be38557b2..11ea070761 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,6 +20,7 @@ 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.
@@ -34,6 +35,27 @@ 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/protocol/ConnectProtocolAdapter.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/ConnectProtocolAdapter.java
new file mode 100644
index 0000000000..ef257f6999
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/ConnectProtocolAdapter.java
@@ -0,0 +1,189 @@
+/*
+ * 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.protocol;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.github.agenticai.connect.Connect;
+import io.github.agenticai.connect.ConversionContext;
+import io.github.agenticai.connect.ConversionException;
+import io.github.agenticai.connect.ConversionResult;
+import io.github.agenticai.connect.Diagnostic;
+import io.github.agenticai.connect.Format;
+import io.github.agenticai.connect.Json;
+import io.github.agenticai.connect.Options;
+import io.github.agenticai.connect.ProviderProfile;
+import io.github.agenticai.connect.ResponseStreamState;
+import io.github.agenticai.connect.StreamOptions;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Request-scoped bridge between ShenYu's reactive HTTP boundary and Connect's 
JSON codecs.
+ * No transport, routing, credentials, retry or billing policy is delegated to 
Connect.
+ */
+public final class ConnectProtocolAdapter {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ConnectProtocolAdapter.class);
+
+    private static final ObjectMapper MAPPER = new 
ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
+
+    private static final Connect CONNECT = new Connect();
+
+    private static final int MAX_CONVERSION_BYTES = 5 * 1024 * 1024;
+
+    private final Format clientFormat;
+
+    private final ConversionContext context;
+
+    /**
+     * Create an isolated context for a request and its response, never a 
singleton context.
+     *
+     * @param protocol client protocol identifier
+     * @param upstreamModel authoritative routed model, or null to retain the 
client model
+     * @param profile explicitly selected provider dialect, or null for 
generic Chat
+     */
+    public ConnectProtocolAdapter(final String protocol, final String 
upstreamModel, final ProviderProfile profile) {
+        clientFormat = OpenAiResponses.NAME.equals(protocol) ? 
Format.OPENAI_RESPONSES : Format.OPENAI_CHAT;
+        Options options = new Options();
+        options.toolLossPolicy = Options.LossPolicy.SAFE;
+        options.providerProfile = profile;
+        context = new ConversionContext(options);
+        context.upstreamModel = Objects.isNull(upstreamModel) ? "" : 
upstreamModel;
+    }
+
+    /**
+     * Convert the final gateway-configured payload to Chat and apply the 
provider profile once.
+     *
+     * @param payload copied client payload with gateway overrides applied
+     * @return converted payload
+     */
+    public ObjectNode convertRequest(final ObjectNode payload) {
+        try {
+            ConversionResult result = CONNECT.convertRequest(clientFormat, 
Format.OPENAI_CHAT, Json.object(payload.toString()), context);
+            reportDiagnostics(result.diagnostics());
+            return (ObjectNode) MAPPER.readTree(result.json());
+        } catch (Exception error) {
+            if (error instanceof ConversionException) {
+                reportDiagnostics(((ConversionException) error).diagnostics());
+            }
+            throw new AiProxyException(AiProxyException.Kind.REQUEST, "AI 
request protocol conversion failed", error);
+        }
+    }
+
+    /**
+     * Convert successful Responses bodies; preserve Chat and upstream HTTP 
errors unchanged.
+     *
+     * @param response upstream response with an unconsumed body
+     * @param stream effective streaming mode
+     * @return adapted response without subscribing or blocking
+     */
+    public AiUpstreamResponse convertResponse(final AiUpstreamResponse 
response, final boolean stream) {
+        if (clientFormat == Format.OPENAI_CHAT || response.getStatusCode() < 
200 || response.getStatusCode() >= 300) {
+            return response;
+        }
+        if (stream) {
+            // Reuse ShenYu's incremental SSE framing and content-type 
validation, not a library HTTP client.
+            AiUpstreamResponse framed = new 
OpenAiChat().encodeResponse(response, true);
+            return new AiUpstreamResponse(response.getStatusCode(), 
responseHeaders(response, "text/event-stream"), 
convertStream(framed.getBody()));
+        }
+        Flux<byte[]> body = 
response.getBody().reduceWith(ByteArrayOutputStream::new, (buffer, bytes) -> {
+            if (bytes.length > MAX_CONVERSION_BYTES - buffer.size()) {
+                throw new AiProxyException(AiProxyException.Kind.PROTOCOL, "AI 
response exceeds the conversion size limit", null);
+            }
+            buffer.writeBytes(bytes);
+            return buffer;
+        }).map(buffer -> 
convertCompleteResponse(buffer.toString(StandardCharsets.UTF_8))).flux();
+        return new AiUpstreamResponse(response.getStatusCode(), 
responseHeaders(response, "application/json"), body);
+    }
+
+    private byte[] convertCompleteResponse(final String body) {
+        try {
+            JsonNode payload = MAPPER.readTree(body);
+            if (Objects.isNull(payload) || !payload.isObject()) {
+                throw new IllegalArgumentException("Expected an AI response 
object");
+            }
+            ConversionResult result = 
CONNECT.convertResponse(Format.OPENAI_CHAT, clientFormat, 
Json.object(payload.toString()), context);
+            reportDiagnostics(result.diagnostics());
+            return result.json().getBytes(StandardCharsets.UTF_8);
+        } catch (Exception error) {
+            throw new AiProxyException(AiProxyException.Kind.PROTOCOL, "AI 
response protocol conversion failed", error);
+        }
+    }
+
+    private Flux<byte[]> convertStream(final Flux<byte[]> framed) {
+        return Flux.defer(() -> {
+            StreamOptions options = new StreamOptions();
+            options.emitSequenceNumber = true;
+            ResponseStreamState state = 
CONNECT.newResponseStream(Format.OPENAI_CHAT, clientFormat, options, context);
+            int[] size = {0};
+            return framed.concatMapIterable(bytes -> {
+                // The framer produces exactly one validated event, retaining 
multiline JSON.
+                String event = new String(bytes, StandardCharsets.UTF_8);
+                String payload = event.substring(6, event.length() - 
2).replace("\ndata: ", "\n");
+                if ("[DONE]".equals(payload)) {
+                    return List.<ConversionResult>of();
+                }
+                if (bytes.length > MAX_CONVERSION_BYTES - size[0]) {
+                    throw new AiProxyException(AiProxyException.Kind.PROTOCOL, 
"AI stream exceeds the conversion size limit", null);
+                }
+                size[0] += bytes.length;
+                return state.accept(payload);
+            }, 1).concatWith(Flux.defer(() -> {
+                List<ConversionResult> terminal = state.finish();
+                reportDiagnostics(state.diagnostics());
+                return Flux.fromIterable(terminal);
+            })).map(this::frameEvent)
+                    .onErrorMap(error -> !(error instanceof AiProxyException),
+                            error -> new 
AiProxyException(AiProxyException.Kind.PROTOCOL, "AI stream protocol conversion 
failed", error));
+        });
+    }
+
+    private byte[] frameEvent(final ConversionResult result) {
+        String type = result.value().get("type").getAsString();
+        return ("event: " + type + "\ndata: " + result.json() + 
"\n\n").getBytes(StandardCharsets.UTF_8);
+    }
+
+    private Map<String, List<String>> responseHeaders(final AiUpstreamResponse 
response, final String contentType) {
+        Map<String, List<String>> headers = new 
LinkedHashMap<>(response.getHeaders());
+        headers.keySet().removeIf(name -> 
"Content-Length".equalsIgnoreCase(name) || "Content-Type".equalsIgnoreCase(name)
+                || "ETag".equalsIgnoreCase(name) || 
"Content-MD5".equalsIgnoreCase(name) || "Digest".equalsIgnoreCase(name));
+        headers.put("Content-Type", List.of(contentType));
+        return headers;
+    }
+
+    private void reportDiagnostics(final List<Diagnostic> diagnostics) {
+        for (Diagnostic diagnostic : diagnostics) {
+            // Never log payloads, credentials, private reasoning or library 
diagnostic messages.
+            LOG.warn("AI conversion diagnostic: code={}, severity={}, from={}, 
to={}",
+                    diagnostic.code(), diagnostic.severity(), 
diagnostic.from(), diagnostic.to());
+        }
+    }
+}
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiResponses.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiResponses.java
new file mode 100644
index 0000000000..d578125e7f
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiResponses.java
@@ -0,0 +1,75 @@
+/*
+ * 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.protocol;
+
+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.proxy.enhanced.model.ShenyuAiRequest;
+
+import java.util.Objects;
+
+/**
+ * Responses request boundary; Connect owns conversion of its protocol fields.
+ */
+public final class OpenAiResponses implements AiProxyProtocol {
+
+    /**
+     * Selector handle protocol identifier for Responses clients.
+     */
+    public static final String NAME = "openai-responses";
+
+    @Override
+    public String name() {
+        return NAME;
+    }
+
+    @Override
+    public boolean matches(final JsonNode payload) {
+        return Objects.nonNull(payload) && payload.isObject() && 
payload.has("input") && !payload.has("messages");
+    }
+
+    @Override
+    public ShenyuAiRequest decodeRequest(final String rawBody, final 
ObjectNode payload) {
+        JsonNode input = payload.get("input");
+        if (Objects.isNull(input) || !(input.isTextual() || input.isArray())) {
+            throw new ShenyuException("OpenAI Responses input must be a string 
or array");
+        }
+        JsonNode stream = payload.get("stream");
+        if (Objects.nonNull(stream) && !stream.isNull() && 
!stream.isBoolean()) {
+            throw new ShenyuException("OpenAI Responses stream must be 
boolean");
+        }
+        JsonNode model = payload.get("model");
+        if (Objects.nonNull(model) && !model.isNull() && !model.isTextual()) {
+            throw new ShenyuException("OpenAI Responses model must be a 
string");
+        }
+        JsonNode maxTokens = payload.get("max_output_tokens");
+        if (Objects.nonNull(maxTokens) && !maxTokens.isNull()
+                && (!maxTokens.isIntegralNumber() || 
!maxTokens.canConvertToInt() || maxTokens.intValue() <= 0)) {
+            throw new ShenyuException("OpenAI Responses max_output_tokens must 
be a positive integer");
+        }
+        ShenyuAiRequest request = new ShenyuAiRequest();
+        request.setProtocol(NAME);
+        request.setRawBody(rawBody);
+        request.setRawPayload(payload.deepCopy());
+        request.setModel(Objects.isNull(model) || model.isNull() ? null : 
model.textValue());
+        request.setStream(Objects.isNull(stream) || stream.isNull() ? null : 
stream.booleanValue());
+        request.setMaxTokens(Objects.isNull(maxTokens) || maxTokens.isNull() ? 
null : maxTokens.intValue());
+        return request;
+    }
+}
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 5ced8e7870..c41f491a56 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
@@ -19,11 +19,14 @@ package org.apache.shenyu.plugin.ai.proxy.enhanced.provider;
 
 import com.fasterxml.jackson.databind.JsonNode;
 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.model.AiUpstreamRequest;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.model.ShenyuAiRequest;
 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.protocol.ConnectProtocolAdapter;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
@@ -44,6 +47,24 @@ public final class OpenAi implements AiProxyProvider {
 
     private static final String CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
 
+    private final ProviderProfile profile;
+
+    /**
+     * Create a generic OpenAI Chat upstream without provider-specific 
adaptation.
+     */
+    public OpenAi() {
+        this(null);
+    }
+
+    /**
+     * Create a Chat upstream with an explicitly selected provider dialect.
+     *
+     * @param profile provider profile, or null for generic OpenAI 
compatibility
+     */
+    public OpenAi(final ProviderProfile profile) {
+        this.profile = profile;
+    }
+
     @Override
     public String name() {
         return NAME;
@@ -52,7 +73,7 @@ public final class OpenAi implements AiProxyProvider {
     @Override
     public AiUpstreamRequest buildRequest(final ShenyuAiRequest request, final 
AiCommonConfig config) {
         validateConfig(config);
-        if (!OpenAiChat.NAME.equals(request.getProtocol())) {
+        if (!supports(request.getProtocol())) {
             throw new IllegalArgumentException(
                     "Unsupported OpenAI protocol: " + request.getProtocol());
         }
@@ -61,17 +82,17 @@ public final class OpenAi implements AiProxyProvider {
         boolean changed = payload.has("fallbackConfig");
         payload.remove("fallbackConfig");
 
-        if (Objects.nonNull(config.getModel())) {
-            payload.put("model", config.getModel());
-            changed = true;
-        }
         if (Objects.nonNull(config.getTemperature())) {
             payload.put("temperature", config.getTemperature());
             changed = true;
         }
         if (Objects.nonNull(config.getMaxTokens())) {
-            payload.put("max_tokens", config.getMaxTokens());
-            payload.put("max_completion_tokens", config.getMaxTokens());
+            if (OpenAiResponses.NAME.equals(request.getProtocol())) {
+                payload.put("max_output_tokens", config.getMaxTokens());
+            } else {
+                payload.put("max_tokens", config.getMaxTokens());
+                payload.put("max_completion_tokens", config.getMaxTokens());
+            }
             changed = true;
         }
 
@@ -82,8 +103,12 @@ public final class OpenAi implements AiProxyProvider {
             payload.put("stream", stream);
             changed = true;
         }
+        // Gateway overrides precede Connect's final protocol/provider 
adaptation.
+        final ConnectProtocolAdapter adapter = new 
ConnectProtocolAdapter(request.getProtocol(), config.getModel(), profile);
+        final ObjectNode converted = adapter.convertRequest(payload);
+        changed = changed || !payload.equals(converted);
         if (stream) {
-            changed = enableStreamUsage(payload) || changed;
+            changed = enableStreamUsage(converted) || changed;
         }
 
         final Map<String, List<String>> headers =
@@ -107,8 +132,9 @@ public final class OpenAi implements AiProxyProvider {
                 config.getBaseUrl(),
                 CHAT_COMPLETIONS_PATH));
         upstreamRequest.setHeaders(headers);
-        final String body = changed ? payload.toString() : 
request.getRawBody();
+        final String body = changed ? converted.toString() : 
request.getRawBody();
         upstreamRequest.setBody(body.getBytes(StandardCharsets.UTF_8));
+        upstreamRequest.setResponseAdapter(response -> 
adapter.convertResponse(response, stream));
 
         return upstreamRequest;
     }
@@ -158,6 +184,6 @@ public final class OpenAi implements AiProxyProvider {
 
     @Override
     public boolean supports(final String protocol) {
-        return OpenAiChat.NAME.equals(protocol);
+        return OpenAiChat.NAME.equals(protocol) || 
OpenAiResponses.NAME.equals(protocol);
     }
 }
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 5761ac63f5..06daf73886 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
@@ -17,6 +17,7 @@
 
 package org.apache.shenyu.plugin.ai.proxy.enhanced.provider;
 
+import io.github.agenticai.connect.ProviderProfile;
 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.ShenyuAiRequest;
@@ -28,10 +29,12 @@ public final class OpenAiCompatible implements 
AiProxyProvider {
 
     private final String name;
 
-    private final OpenAi delegate = new OpenAi();
+    private final OpenAi delegate;
 
     public OpenAiCompatible(final String name) {
         this.name = name;
+        // Only an explicitly selected DeepSeek provider enables its dialect 
rules.
+        this.delegate = new OpenAi("DeepSeek".equalsIgnoreCase(name) ? 
ProviderProfile.deepSeekChat() : null);
     }
 
     @Override
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 90da0a59b5..b25664b097 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
@@ -87,6 +87,8 @@ public final class AiProxyEngine {
             Optional<AiCommonConfig> fallback = fallbackConfig.map(config -> {
                 AiCommonConfig copy = new AiCommonConfig(config);
                 copy.setStream(primary.stream());
+                // Fallback changes the upstream, never the already selected 
client protocol.
+                copy.setProtocol(primary.protocol().name());
                 return copy;
             });
             return attempt(requestBody, primary, fallback, primary.stream() ? 
1 : 3, responseConsumer, new AtomicBoolean());
@@ -114,7 +116,7 @@ public final class AiProxyEngine {
                         // Release this response without accumulating the 
error body before another attempt.
                         return response.getBody().then(Mono.error(new 
UpstreamStatusFailure(status)));
                     }
-                    AiUpstreamResponse adapted = 
prepared.provider().adaptResponse(response);
+                    AiUpstreamResponse adapted = 
prepared.request().adaptResponse(prepared.provider().adaptResponse(response));
                     AiUpstreamResponse encoded = 
prepared.protocol().encodeResponse(adapted, prepared.stream());
                     return encoded.getBody().switchOnFirst((signal, body) -> {
                         if (signal.isOnError()) {
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 5c82bc4fc4..f3e2616b76 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
@@ -17,6 +17,8 @@
 
 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.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
@@ -24,6 +26,7 @@ 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.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.AiProxyProviderFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAi;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.service.AiProxyConfigService;
@@ -47,6 +50,7 @@ import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -59,6 +63,76 @@ class AiProxyHttpCompatibilityTest {
 
     private static final Duration TIMEOUT = Duration.ofSeconds(5);
 
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final String CHAT_REPLY = 
"{\"id\":\"chatcmpl-http\",\"model\":\"model\",\"choices\":[{\"index\":0,"
+            + 
"\"message\":{\"role\":\"assistant\",\"content\":\"hello\"},\"finish_reason\":\"stop\"}],"
+            + 
"\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2,\"total_tokens\":5}}";
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void convertsResponsesThroughThePluginAndRealHttpTransport(final boolean 
stream) throws Exception {
+        AtomicReference<String> forwarded = new AtomicReference<>();
+        String reply = stream ? "data: 
{\"id\":\"chatcmpl-http\",\"model\":\"model\",\"choices\":[{\"index\":0,"
+                + 
"\"delta\":{\"role\":\"assistant\",\"content\":\"hello\"},\"finish_reason\":\"stop\"}]}\n\n"
+                + "data: 
{\"choices\":[],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2}}\n\ndata:
 [DONE]\n\n" : CHAT_REPLY;
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) -> {
+            assertEquals("/v1/chat/completions", request.uri());
+            assertEquals("Bearer real-key", 
request.requestHeaders().get("Authorization"));
+            return request.receive().aggregate().asString().flatMap(body -> {
+                forwarded.set(body);
+                return response.header("Content-Type", stream ? 
"text/event-stream" : "application/json")
+                        .header("ETag", 
"old-chat-representation").header("X-Upstream", 
"retained").sendString(Flux.just(reply)).then();
+            });
+        }).bindNow(TIMEOUT);
+        try {
+            MockServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/v1/responses")
+                    
.body("{\"model\":\"client-alias\",\"input\":\"hi\",\"max_output_tokens\":12,\"stream\":"
 + stream + "}"));
+            StepVerifier.create(plugin(server, stream, 
OpenAiResponses.NAME).doExecute(exchange, mock(ShenyuPluginChain.class), 
selector(), new RuleData())).verifyComplete();
+            JsonNode upstream = MAPPER.readTree(forwarded.get());
+            assertEquals("model", upstream.get("model").asText());
+            assertEquals("hi", upstream.at("/messages/0/content").asText());
+            assertEquals(12, upstream.get("max_completion_tokens").asInt());
+            assertTrue(!upstream.has("input"));
+            String actual = 
exchange.getResponse().getBodyAsString().block(TIMEOUT);
+            assertEquals(200, exchange.getResponse().getStatusCode().value());
+            assertEquals("retained", 
exchange.getResponse().getHeaders().getFirst("X-Upstream"));
+            
assertTrue(!exchange.getResponse().getHeaders().containsKey("ETag"));
+            if (stream) {
+                assertTrue(actual.contains("event: 
response.output_text.delta\n"));
+                assertEquals(1, actual.split("event: response.completed\n", 
-1).length - 1);
+                assertTrue(actual.contains("\"input_tokens\":3"));
+                assertTrue(!actual.contains("[DONE]"));
+                
assertTrue(upstream.at("/stream_options/include_usage").asBoolean());
+            } else {
+                JsonNode result = MAPPER.readTree(actual);
+                assertEquals("response", result.get("object").asText());
+                assertEquals("hello", 
result.at("/output/0/content/0/text").asText());
+                assertEquals(5, result.at("/usage/total_tokens").asInt());
+            }
+        } finally {
+            server.disposeNow(TIMEOUT);
+        }
+    }
+
+    @Test
+    void rejectsResponsesStateBeforeOpeningAnUpstreamConnection() {
+        AtomicBoolean called = new AtomicBoolean();
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) -> {
+            called.set(true);
+            return response.send();
+        }).bindNow(TIMEOUT);
+        try {
+            MockServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/v1/responses")
+                    
.body("{\"input\":\"hi\",\"previous_response_id\":\"resp_previous\"}"));
+            StepVerifier.create(plugin(server, false, 
OpenAiResponses.NAME).doExecute(exchange, mock(ShenyuPluginChain.class), 
selector(), new RuleData())).verifyComplete();
+            assertEquals(400, exchange.getResponse().getStatusCode().value());
+            assertTrue(!called.get());
+        } finally {
+            server.disposeNow(TIMEOUT);
+        }
+    }
+
     @ParameterizedTest
     @ValueSource(booleans = {false, true})
     void 
forwardsTheFirstChunkWithoutWaitingForCompletionAndCancelsTheSocket(final 
boolean stream) {
@@ -87,6 +161,32 @@ class AiProxyHttpCompatibilityTest {
         }
     }
 
+    @Test
+    void cancelsTheResponsesUpstreamSocketWithoutCompletingTheStream() {
+        AtomicBoolean cancelled = new AtomicBoolean();
+        String first = "data: 
{\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hello\"}}]}\n\n";
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) ->
+                response.header("Content-Type", "text/event-stream")
+                        
.sendString(Flux.just(first).concatWith(Flux.never()).doOnCancel(() -> 
cancelled.set(true)))).bindNow(TIMEOUT);
+        try {
+            MockServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.post("/v1/responses")
+                    .body("{\"input\":\"hi\",\"stream\":true}"));
+            List<String> received = new 
java.util.concurrent.CopyOnWriteArrayList<>();
+            exchange.getResponse().setWriteHandler(body -> 
body.doOnNext(buffer -> {
+                received.add(buffer.toString(StandardCharsets.UTF_8));
+                DataBufferUtils.release(buffer);
+            }).then());
+            StepVerifier.create(plugin(server, true, 
OpenAiResponses.NAME).doExecute(exchange, mock(ShenyuPluginChain.class), 
selector(), new RuleData()))
+                    .then(() -> 
org.awaitility.Awaitility.await().atMost(TIMEOUT)
+                            .until(() -> received.stream().anyMatch(event -> 
event.startsWith("event: response.output_text.delta\n"))))
+                    .thenCancel().verify(TIMEOUT);
+            
org.awaitility.Awaitility.await().atMost(TIMEOUT).untilTrue(cancelled);
+            assertTrue(received.stream().noneMatch(event -> 
event.startsWith("event: response.completed\n")));
+        } finally {
+            server.disposeNow(TIMEOUT);
+        }
+    }
+
     @Test
     void forwardsNormalBodiesBeyondTheDefaultWebClientCodecLimit() {
         String responseBody = "{\"unknown\":\"" + "x".repeat(512 * 1024) + 
"\"}";
@@ -105,15 +205,20 @@ class AiProxyHttpCompatibilityTest {
     }
 
     private AiProxyPlugin plugin(final DisposableServer server, final boolean 
stream) {
+        return plugin(server, stream, OpenAiChat.NAME);
+    }
+
+    private AiProxyPlugin plugin(final DisposableServer server, final boolean 
stream, final String protocol) {
         final AiProxyPluginHandler handler = new AiProxyPluginHandler();
         AiProxyHandle handle = new AiProxyHandle();
         handle.setProvider("OpenAI");
         handle.setBaseUrl("http://127.0.0.1:"; + server.port());
         handle.setApiKey("real-key");
         handle.setStream(stream);
+        handle.setProtocol(protocol);
         handle.setModel("model");
         
handler.getSelectorCachedHandle().cachedHandle(CacheKeyUtils.INST.getKey("http-selector",
 Constants.DEFAULT_RULE), handle);
-        AiProxyEngine engine = new AiProxyEngine(new 
AiProxyProtocolFactory(List.of(new OpenAiChat())), new 
AiProxyProviderFactory(List.of(new OpenAi())),
+        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);
     }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/ConnectProtocolAdapterTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/ConnectProtocolAdapterTest.java
new file mode 100644
index 0000000000..dab3b60d1f
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/ConnectProtocolAdapterTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.protocol;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.github.agenticai.connect.ProviderProfile;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Library contracts at ShenYu's JSON/reactive boundary, without live-provider 
calls.
+ */
+class ConnectProtocolAdapterTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final String RESPONSE = 
"{\"id\":\"chatcmpl-test\",\"model\":\"upstream\",\"choices\":[{\"index\":0,"
+            + 
"\"message\":{\"role\":\"assistant\",\"content\":\"hello\"},\"finish_reason\":\"stop\"}],"
+            + 
"\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2,\"total_tokens\":5}}";
+
+    private static final String CHUNK = 
"{\"id\":\"chatcmpl-test\",\"model\":\"upstream\",\"choices\":[{\"index\":0,"
+            + 
"\"delta\":{\"role\":\"assistant\",\"content\":\"hello\"},\"finish_reason\":null}]}";
+
+    @Test
+    void delegatesRequestConversionAndProviderAdaptationExactlyOnce() throws 
Exception {
+        AtomicInteger calls = new AtomicInteger();
+        ProviderProfile profile = (payload, adaptation) -> {
+            calls.incrementAndGet();
+            assertEquals("routed", payload.get("model").getAsString());
+            payload.addProperty("custom", true);
+            adaptation.warning("custom_profile", "custom", "Test profile");
+        };
+        ObjectNode input = object("{\"model\":\"alias\",\"instructions\":\"be 
helpful\",\"input\":\"hello\",\"max_output_tokens\":12}");
+        ObjectNode before = input.deepCopy();
+        ObjectNode result = new ConnectProtocolAdapter(OpenAiResponses.NAME, 
"routed", profile).convertRequest(input);
+        assertEquals(1, calls.get());
+        assertEquals(before, input);
+        assertEquals("system", result.at("/messages/0/role").asText());
+        assertEquals("hello", result.at("/messages/1/content").asText());
+        assertEquals(12, result.get("max_completion_tokens").asInt());
+        assertTrue(result.get("custom").asBoolean());
+    }
+
+    @Test
+    void keepsGenericIdentityFieldsAndRejectsUnrepresentableResponsesState() 
throws Exception {
+        ObjectNode input = 
object("{\"messages\":[{\"role\":\"user\"}],\"vendor\":true,\"content\":null}");
+        assertEquals(input, new ConnectProtocolAdapter(OpenAiChat.NAME, null, 
null).convertRequest(input));
+        ConnectProtocolAdapter adapter = adapter();
+        for (String body : 
List.of("{\"model\":\"x\",\"input\":\"hi\",\"previous_response_id\":\"resp_old\"}",
 "{\"input\":\"hi\"}")) {
+            AiProxyException error = assertThrows(AiProxyException.class, () 
-> adapter.convertRequest(object(body)));
+            assertEquals(AiProxyException.Kind.REQUEST, error.getKind());
+        }
+    }
+
+    @Test
+    void adaptsDeepSeekTokensAndReasoningWithoutGuessingFromAModel() throws 
Exception {
+        ObjectNode input = 
object("{\"model\":\"deepseek-model\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],"
+                + 
"\"max_completion_tokens\":20,\"max_tokens\":10,\"reasoning_effort\":\"medium\"}");
+        assertEquals(input, new ConnectProtocolAdapter(OpenAiChat.NAME, null, 
null).convertRequest(input));
+        ObjectNode result = new ConnectProtocolAdapter(OpenAiChat.NAME, null, 
ProviderProfile.deepSeekChat()).convertRequest(input);
+        assertEquals(20, result.get("max_tokens").asInt());
+        assertFalse(result.has("max_completion_tokens"));
+        assertEquals("high", result.get("reasoning_effort").asText());
+        assertEquals("enabled", result.at("/thinking/type").asText());
+        input.put("tool_choice", "required");
+        assertThrows(AiProxyException.class,
+                () -> new ConnectProtocolAdapter(OpenAiChat.NAME, null, 
ProviderProfile.deepSeekChat()).convertRequest(input));
+    }
+
+    @Test
+    void convertsCompleteResponsesAndRemovesStaleRepresentationHeaders() 
throws Exception {
+        AiUpstreamResponse upstream = new AiUpstreamResponse(200, 
Map.of("content-length", List.of("123"), "ETag", List.of("old"),
+                "Content-MD5", List.of("old"), "Digest", List.of("old"), 
"X-Request-ID", List.of("kept")),
+                Flux.just(bytes(RESPONSE.substring(0, 20)), 
bytes(RESPONSE.substring(20))));
+        AiUpstreamResponse converted = adapter().convertResponse(upstream, 
false);
+        assertEquals(Map.of("Content-Type", List.of("application/json"), 
"X-Request-ID", List.of("kept")), converted.getHeaders());
+        JsonNode result = 
MAPPER.readTree(converted.getBody().single().block(Duration.ofSeconds(3)));
+        assertEquals("response", result.get("object").asText());
+        assertEquals("hello", result.at("/output/0/content/0/text").asText());
+        assertEquals(3, result.at("/usage/input_tokens").asInt());
+        assertEquals(2, result.at("/usage/output_tokens").asInt());
+    }
+
+    @Test
+    void restoresCustomToolsUsingTheOriginalRequestContext() throws Exception {
+        ConnectProtocolAdapter adapter = adapter();
+        ObjectNode converted = 
adapter.convertRequest(object("{\"model\":\"x\",\"input\":\"hi\",\"tools\":[{\"type\":\"custom\",\"name\":\"execute\"}]}"));
+        String name = converted.at("/tools/0/function/name").asText();
+        String body = 
"{\"model\":\"x\",\"choices\":[{\"index\":0,\"finish_reason\":\"tool_calls\",\"message\":{\"role\":\"assistant\","
+                + 
"\"tool_calls\":[{\"id\":\"call_one\",\"type\":\"function\",\"function\":{\"name\":\""
 + name + "\",\"arguments\":\"{\\\"input\\\":\\\"print(1)\\\"}\"}}]}}]}";
+        JsonNode result = 
MAPPER.readTree(adapter.convertResponse(response(200, Flux.just(bytes(body))), 
false).getBody().single().block());
+        assertEquals("custom_tool_call", result.at("/output/0/type").asText());
+        assertEquals("execute", result.at("/output/0/name").asText());
+        assertEquals("print(1)", result.at("/output/0/input").asText());
+        JsonNode unrelated = 
MAPPER.readTree(adapter().convertResponse(response(200, 
Flux.just(bytes(body))), false).getBody().single().block());
+        assertEquals("function_call", unrelated.at("/output/0/type").asText());
+    }
+
+    @Test
+    void restoresFragmentedCustomToolStreamsWithoutCrossRequestState() throws 
Exception {
+        ConnectProtocolAdapter custom = adapter();
+        ObjectNode request = 
custom.convertRequest(object("{\"model\":\"x\",\"input\":\"hi\",\"tools\":[{\"type\":\"custom\",\"name\":\"execute\"}]}"));
+        String name = request.at("/tools/0/function/name").asText();
+        ObjectNode start = 
object("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_one\",\"type\":\"function\",\"function\":{}}]}}]}");
+        ObjectNode function = (ObjectNode) 
start.at("/choices/0/delta/tool_calls/0/function");
+        function.put("name", name);
+        function.put("arguments", "{\"input\":\"print(");
+        ObjectNode next = start.deepCopy();
+        ObjectNode continuation = (ObjectNode) 
next.at("/choices/0/delta/tool_calls/0");
+        continuation.remove(List.of("id", "type"));
+        ((ObjectNode) continuation.get("function")).remove("name");
+        ((ObjectNode) continuation.get("function")).put("arguments", "1)\"}");
+        Flux<byte[]> chunks = Flux.just(bytes("data: " + start + "\n\n"), 
bytes("data: " + next + "\n\n"),
+                bytes("data: 
{\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\n"));
+        Flux<String> restored = custom.convertResponse(response(200, chunks), 
true).getBody().map(bytes -> new String(bytes, StandardCharsets.UTF_8));
+        Flux<String> isolated = adapter().convertResponse(response(200, 
chunks), true).getBody().map(bytes -> new String(bytes, 
StandardCharsets.UTF_8));
+        List<List<String>> results = 
reactor.core.publisher.Mono.zip(restored.collectList(), isolated.collectList(), 
List::of).block(Duration.ofSeconds(3));
+        JsonNode customResult = terminalResponse(results.get(0));
+        JsonNode genericResult = terminalResponse(results.get(1));
+        assertEquals("custom_tool_call", 
customResult.at("/response/output/0/type").asText());
+        assertEquals("print(1)", 
customResult.at("/response/output/0/input").asText());
+        assertEquals("function_call", 
genericResult.at("/response/output/0/type").asText());
+        assertEquals("{\"input\":\"print(1)\"}", 
genericResult.at("/response/output/0/arguments").asText());
+    }
+
+    private JsonNode terminalResponse(final List<String> events) throws 
Exception {
+        String terminal = events.get(events.size() - 1);
+        return MAPPER.readTree(terminal.substring(terminal.indexOf("data: ") + 
6).trim());
+    }
+
+    @Test
+    void preservesHttpErrorsAndChatResponsesWithoutConsumingThem() {
+        for (int status : List.of(199, 400, 500)) {
+            AiUpstreamResponse upstream = response(status, Flux.error(new 
AssertionError("Body must not be consumed")));
+            assertSame(upstream, adapter().convertResponse(upstream, false));
+            assertSame(upstream, adapter().convertResponse(upstream, true));
+        }
+        AiUpstreamResponse upstream = response(200, Flux.empty());
+        assertSame(upstream, new ConnectProtocolAdapter(OpenAiChat.NAME, null, 
null).convertResponse(upstream, true));
+    }
+
+    @Test
+    void rejectsMalformedEmptyTrailingAndOversizedCompleteResponses() {
+        for (String body : List.of("invalid", "[]", "", "null", RESPONSE + " 
{}", "x".repeat(5 * 1024 * 1024 + 1))) {
+            StepVerifier.create(adapter().convertResponse(response(200, 
Flux.just(bytes(body))), false).getBody())
+                    .expectErrorMatches(error -> error instanceof 
AiProxyException && ((AiProxyException) error).getKind() == 
AiProxyException.Kind.PROTOCOL).verify();
+        }
+        StepVerifier.create(adapter().convertResponse(response(200, 
Flux.empty()), false).getBody()).expectError(AiProxyException.class).verify();
+    }
+
+    @Test
+    void convertsFragmentedMultilineSseAndProducesOneTerminalEvent() {
+        String wire = ": heartbeat\r\ndata: {\r\ndata: " + CHUNK.substring(1) 
+ "\r\n\r\n"
+                + "data: 
{\"choices\":[],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2}}\r\n\r\ndata:
 [DONE]\r\n\r\n";
+        List<byte[]> fragments = new ArrayList<>();
+        for (byte value : bytes(wire)) {
+            fragments.add(new byte[] {value});
+        }
+        List<String> events = 
stream(Flux.fromIterable(fragments)).collectList().block(Duration.ofSeconds(3));
+        assertTrue(events.stream().anyMatch(event -> event.startsWith("event: 
response.output_text.delta\n") && event.contains("hello")));
+        assertEquals(1, events.stream().filter(event -> 
event.startsWith("event: response.completed\n")).count());
+        assertTrue(events.get(events.size() - 
1).contains("\"input_tokens\":3"));
+        assertTrue(events.stream().allMatch(event -> 
event.contains("\"sequence_number\":") && !event.contains("[DONE]")));
+    }
+
+    @Test
+    void isolatesStreamStateOnResubscriptionAndCancelsWithoutATerminalEvent() {
+        Flux<String> reusable = stream(Flux.just(bytes("data: " + CHUNK + 
"\n\n")));
+        for (int attempt = 0; attempt < 2; attempt++) {
+            List<String> events = reusable.collectList().block();
+            assertEquals(1, events.stream().filter(event -> 
event.startsWith("event: response.completed\n")).count());
+        }
+        AtomicBoolean cancelled = new AtomicBoolean();
+        Flux<String> live = stream(Flux.just(bytes("data: " + CHUNK + 
"\n\n")).concatWith(Flux.never()).doOnCancel(() -> cancelled.set(true)));
+        StepVerifier.create(live, 0).thenRequest(1).expectNextMatches(event -> 
event.startsWith("event: response.created\n"))
+                .thenCancel().verify(Duration.ofSeconds(3));
+        assertTrue(cancelled.get());
+    }
+
+    @Test
+    void doesNotSynthesizeSuccessAfterMalformedEventsOrTransportFailure() {
+        for (Flux<byte[]> body : List.of(Flux.just(bytes("data: " + CHUNK + 
"\n\ndata: invalid\n\n")),
+                Flux.just(bytes("data: " + CHUNK + 
"\n\n")).concatWith(Flux.error(new IllegalStateException("socket closed"))))) {
+            List<String> events = new ArrayList<>();
+            
StepVerifier.create(stream(body).doOnNext(events::add)).thenConsumeWhile(event 
-> !event.startsWith("event: response.completed\n"))
+                    .expectError(AiProxyException.class).verify();
+            assertTrue(events.stream().noneMatch(event -> 
event.startsWith("event: response.completed\n")));
+        }
+        AiUpstreamResponse json = new AiUpstreamResponse(200, 
Map.of("Content-Type", List.of("application/json")), 
Flux.just(bytes(RESPONSE)));
+        StepVerifier.create(adapter().convertResponse(json, 
true).getBody()).expectError(AiProxyException.class).verify();
+    }
+
+    @Test
+    void boundsCumulativeConversionStateAndCancelsAtDone() {
+        String large = "data: " + CHUNK.replace("hello", "x".repeat(1024 * 
1024)) + "\n\n";
+        StepVerifier.create(stream(Flux.range(0, 6).map(index -> 
bytes(large))))
+                .thenConsumeWhile(event -> !event.startsWith("event: 
response.completed\n")).expectError(AiProxyException.class).verify();
+        AtomicBoolean cancelled = new AtomicBoolean();
+        List<String> events = stream(Flux.just(bytes("data: 
[DONE]\n\n")).concatWith(Flux.never()).doOnCancel(() -> cancelled.set(true)))
+                .collectList().block(Duration.ofSeconds(3));
+        assertTrue(cancelled.get());
+        assertEquals(1, events.stream().filter(event -> 
event.startsWith("event: response.completed\n")).count());
+    }
+
+    private ConnectProtocolAdapter adapter() {
+        return new ConnectProtocolAdapter(OpenAiResponses.NAME, null, null);
+    }
+
+    private ObjectNode object(final String json) throws Exception {
+        return (ObjectNode) MAPPER.readTree(json);
+    }
+
+    private AiUpstreamResponse response(final int status, final Flux<byte[]> 
body) {
+        return new AiUpstreamResponse(status, Map.of(), body);
+    }
+
+    private Flux<String> stream(final Flux<byte[]> body) {
+        return adapter().convertResponse(response(200, body), 
true).getBody().map(bytes -> new String(bytes, StandardCharsets.UTF_8));
+    }
+
+    private byte[] bytes(final String value) {
+        return value.getBytes(StandardCharsets.UTF_8);
+    }
+}
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiResponsesTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiResponsesTest.java
new file mode 100644
index 0000000000..776f4b9e2b
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiResponsesTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.protocol;
+
+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.proxy.enhanced.model.ShenyuAiRequest;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Responses shape detection and gateway envelope validation.
+ */
+class OpenAiResponsesTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final OpenAiResponses protocol = new OpenAiResponses();
+
+    @Test
+    void detectsResponsesWithoutAmbiguousChatRequests() throws Exception {
+        assertEquals(OpenAiResponses.NAME, protocol.name());
+        assertFalse(protocol.matches(null));
+        assertFalse(protocol.matches(MAPPER.readTree("[]")));
+        assertFalse(protocol.matches(MAPPER.readTree("{}")));
+        
assertFalse(protocol.matches(MAPPER.readTree("{\"input\":\"hi\",\"messages\":[]}")));
+        assertTrue(protocol.matches(MAPPER.readTree("{\"input\":\"hi\"}")));
+        assertEquals(protocol, new AiProxyProtocolFactory(List.of(new 
OpenAiChat(), protocol)).detect(MAPPER.readTree("{\"input\":\"hi\"}")));
+    }
+
+    @Test
+    void retainsTheOriginalPayloadAndOptionalFields() throws Exception {
+        String body = 
"{\"input\":[],\"model\":\"alias\",\"stream\":true,\"max_output_tokens\":100,\"unknown\":true}";
+        ObjectNode payload = (ObjectNode) MAPPER.readTree(body);
+        ShenyuAiRequest request = protocol.decodeRequest(body, payload);
+        payload.remove("unknown");
+        assertTrue(request.getRawPayload().has("unknown"));
+        assertEquals(body, request.getRawBody());
+        assertEquals("alias", request.getModel());
+        assertEquals(100, request.getMaxTokens());
+        assertTrue(request.isStream());
+        assertTrue(request.hasStream());
+        for (String minimal : List.of("{\"input\":\"hi\"}", 
"{\"input\":\"hi\",\"model\":null,\"stream\":null,\"max_output_tokens\":null}"))
 {
+            ShenyuAiRequest optional = protocol.decodeRequest(minimal, 
(ObjectNode) MAPPER.readTree(minimal));
+            assertNull(optional.getModel());
+            assertNull(optional.getMaxTokens());
+            assertFalse(optional.hasStream());
+        }
+    }
+
+    @Test
+    void rejectsInvalidEnvelopeFields() {
+        for (String body : List.of("{}", "{\"input\":null}", "{\"input\":1}", 
"{\"input\":{}}", "{\"input\":\"hi\",\"stream\":\"yes\"}",
+                "{\"input\":\"hi\",\"model\":42}", 
"{\"input\":\"hi\",\"max_output_tokens\":0}", 
"{\"input\":\"hi\",\"max_output_tokens\":1.5}",
+                "{\"input\":\"hi\",\"max_output_tokens\":99999999999}")) {
+            assertThrows(ShenyuException.class, () -> 
protocol.decodeRequest(body, (ObjectNode) MAPPER.readTree(body)));
+        }
+    }
+}
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 041236636c..b31d6d5116 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
@@ -25,6 +25,7 @@ import 
org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamResponse;
 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.AiProxyProvider;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProviderFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAi;
@@ -45,6 +46,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
@@ -57,6 +59,50 @@ class AiProxyEngineOrchestrationTest {
 
     private static final String BODY = 
"{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"unknown\":42}";
 
+    @Test
+    void convertsResponsesAfterRetryAndFallbackWithoutSharingStreamState() {
+        List<AiUpstreamRequest> requests = new ArrayList<>();
+        AiCommonConfig fallback = config(false, "fallback");
+        fallback.setProvider("DeepSeek");
+        fallback.setMaxTokens(23);
+        AiProxyEngine engine = engine(request -> {
+            requests.add(request);
+            return Mono.just(requests.size() <= 2 ? response(503, 
Flux.just(bytes("busy"))) : response(200,
+                    Flux.just(bytes("data: 
{\"model\":\"fallback\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok\"},\"finish_reason\":\"stop\"}]}\n\n"))));
+        });
+        AiCommonConfig primary = config(false, "main");
+        primary.setProtocol(OpenAiResponses.NAME);
+        List<String> received = new ArrayList<>();
+        
StepVerifier.create(engine.execute("{\"input\":\"hi\",\"stream\":true}", 
primary, Optional.of(fallback),
+                response -> consume(response, received))).verifyComplete();
+        assertEquals(3, requests.size());
+        assertArrayEquals(requests.get(0).getBody(), 
requests.get(1).getBody());
+        String body = new String(requests.get(2).getBody(), 
StandardCharsets.UTF_8);
+        assertTrue(body.contains("\"model\":\"fallback\""));
+        assertTrue(body.contains("\"max_tokens\":23"));
+        assertFalse(body.contains("max_completion_tokens"));
+        assertEquals(1, received.stream().filter(event -> 
event.startsWith("event: response.created\n")).count());
+        assertEquals(1, received.stream().filter(event -> 
event.startsWith("event: response.completed\n")).count());
+    }
+
+    @Test
+    void doesNotRetryResponsesAfterAConvertedEvent() {
+        AtomicInteger calls = new AtomicInteger();
+        List<String> received = new ArrayList<>();
+        AiProxyEngine engine = engine(request -> {
+            calls.incrementAndGet();
+            return Mono.just(response(200, Flux.just(bytes("data: 
{\"choices\":[{\"index\":0,\"delta\":{\"content\":\"first\"}}]}\n\n"))
+                    .concatWith(Flux.error(networkFailure()))));
+        });
+        AiCommonConfig primary = config(true, "main");
+        primary.setProtocol(OpenAiResponses.NAME);
+        
StepVerifier.create(engine.execute("{\"input\":\"hi\",\"stream\":true}", 
primary, Optional.of(config(true, "fallback")),
+                response -> consume(response, 
received))).expectError(AiProxyException.class).verify();
+        assertEquals(1, calls.get());
+        assertTrue(received.stream().anyMatch(event -> 
event.startsWith("event: response.created\n")));
+        assertTrue(received.stream().noneMatch(event -> 
event.startsWith("event: response.completed\n")));
+    }
+
     @Test
     void normalRequestsRetryThreeTimesAndPreserveTheFinalErrorBody() {
         AtomicInteger calls = new AtomicInteger();
@@ -231,7 +277,7 @@ class AiProxyEngineOrchestrationTest {
     }
 
     private AiProxyEngine engine(final AiProxyTransport transport) {
-        return new AiProxyEngine(new AiProxyProtocolFactory(List.of(new 
OpenAiChat())),
+        return new AiProxyEngine(new AiProxyProtocolFactory(List.of(new 
OpenAiChat(), new OpenAiResponses())),
                 new AiProxyProviderFactory(List.of(new OpenAi(), new 
OpenAiCompatible("DeepSeek"))), transport, Duration.ZERO);
     }
 
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 0678e727b0..1708b69a2e 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
@@ -22,6 +22,7 @@ import 
org.apache.shenyu.plugin.ai.proxy.enhanced.handler.AiProxyPluginHandler;
 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.AiProxyProvider;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProviderFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAi;
@@ -119,6 +120,16 @@ public class AiProxyPluginConfiguration {
         return new OpenAiChat();
     }
 
+    /**
+     * OpenAI Responses protocol backed by Connect conversion to Chat 
upstreams.
+     *
+     * @return the OpenAI Responses protocol
+     */
+    @Bean
+    public OpenAiResponses openAiResponses() {
+        return new OpenAiResponses();
+    }
+
     /**
      * Ai proxy protocol factory.
      *
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 5ea1bf813e..80aad054f2 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,8 @@ 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.proxy.enhanced.AiProxyPlugin;
+import 
org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.AiProxyProtocolFactory;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiResponses;
 import 
org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProviderFactory;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.service.AiProxyEngine;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.transport.AiProxyTransport;
@@ -42,8 +44,10 @@ class AiProxyPluginConfigurationTest {
             
org.assertj.core.api.Assertions.assertThat(context).hasNotFailed().hasSingleBean(AiProxyPlugin.class).hasSingleBean(AiProxyEngine.class)
                     .doesNotHaveBean(AiModelFactoryRegistry.class);
             AiProxyProviderFactory factory = 
context.getBean(AiProxyProviderFactory.class);
+            assertSame(context.getBean(OpenAiResponses.class), 
context.getBean(AiProxyProtocolFactory.class).getProtocol(OpenAiResponses.NAME));
             for (String provider : List.of("OpenAI", "DeepSeek", "ALiYun", 
"OpenAPI", "Moonshot")) {
                 assertEquals(provider.toLowerCase(java.util.Locale.ROOT), 
factory.getProvider(provider).name().toLowerCase(java.util.Locale.ROOT));
+                
org.junit.jupiter.api.Assertions.assertTrue(factory.getProvider(provider).supports(OpenAiResponses.NAME));
             }
         });
     }

Reply via email to