This is an automated email from the ASF dual-hosted git repository.

hengyuss 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 1d599d90e8 [ISSUE #7383] Harden provider profiles and capability 
validation. (#7580)
1d599d90e8 is described below

commit 1d599d90e82e3415720b50885823eadc7b93d431
Author: redyyang <[email protected]>
AuthorDate: Thu Oct 15 18:58:23 2026 +0800

    [ISSUE #7383] Harden provider profiles and capability validation. (#7580)
    
    * refactor(ai-proxy): harden provider profiles and capability validation.
    
    * fix(ai-proxy): enforce declared provider capabilities before adaptation
    
    ---------
    
    Co-authored-by: redyyang <[email protected]>
---
 .../shenyu-plugin-ai-proxy/README.md               |  84 ++++-
 .../ai/proxy/enhanced/model/ShenyuAiRequest.java   |  27 ++
 .../ai/proxy/enhanced/protocol/OpenAiChat.java     |   1 +
 .../proxy/enhanced/protocol/OpenAiResponses.java   |   1 +
 .../enhanced/provider/AiProviderCapabilities.java  | 140 +++++++++
 .../proxy/enhanced/provider/AiProxyProvider.java   |  14 +-
 .../plugin/ai/proxy/enhanced/provider/OpenAi.java  | 217 ++++++++-----
 .../proxy/enhanced/provider/OpenAiCompatible.java  |  24 +-
 .../enhanced/provider/OpenAiProviderOptions.java   | 179 +++++++++++
 .../ai/proxy/enhanced/service/AiProxyEngine.java   |   3 +-
 .../enhanced/provider/AiProxyProviderHttpTest.java | 144 +++++++++
 .../provider/OpenAiProviderOptionsTest.java        | 338 +++++++++++++++++++++
 .../service/AiProxyEngineOrchestrationTest.java    |   2 +
 .../proxy/enhanced/service/AiProxyEngineTest.java  | 138 ++++++++-
 .../ai/proxy/AiProxyPluginConfiguration.java       |  10 +
 .../ai/proxy/AiProxyPluginConfigurationTest.java   |  23 +-
 16 files changed, 1256 insertions(+), 89 deletions(-)

diff --git a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
index eda17a845e..477822d853 100644
--- a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
+++ b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/README.md
@@ -97,6 +97,85 @@ 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.
 
+## Provider profiles and capabilities
+
+The explicit Provider registry remains the extension point. Built-in 
identifiers are
+`OpenAI`, `OpenAICompatible`, `DeepSeek`, `ALiYun`, `OpenAPI`, and `Moonshot`; 
separators
+and case are normalized by the registry. Both existing client protocols are 
supported.
+The protocol decoder also declares the request operation (`CHAT_COMPLETION` or
+`RESPONSE`), so a profile can restrict operations separately from protocol 
support.
+Responses clients continue to use conversion to a Chat upstream; these 
declarations
+do not enable native Responses endpoints or other HTTP operations.
+
+`AiProviderCapabilities` declares streaming, function/custom tools, JSON mode, 
JSON
+schema output, stream usage and audio output. The default `supports()` follows 
the
+declared protocols. The engine validates decoded protocol, operation and 
effective
+stream settings before invoking any provider, including custom implementations.
+Providers also validate the final converted payload, including provider 
defaults. Unsupported
+features or operations fail as request errors before HTTP execution. Generic 
capabilities
+describe the service, not a guarantee for every model it hosts; configure a 
narrower
+profile for a model or deployment with additional restrictions. Unknown fields 
that
+need no transformation remain in the payload.
+
+Built-in profiles enumerate their supported operations and features 
explicitly. Adding
+an enum value does not enable it for existing profiles; review and opt in each 
profile.
+
+OpenAI now emits one Chat token-limit field, `max_completion_tokens`. When 
both token
+fields are supplied, the modern field takes precedence; gateway `maxTokens` 
overrides
+it. This follows the [OpenAI Chat 
API](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create).
+Generic compatible profiles preserve the client's field choice unless 
configured
+otherwise. `withMaxTokensField("max_tokens")` selects a legacy upstream 
dialect through
+Connect's provider adaptation phase, after Responses-to-Chat conversion. 
DeepSeek uses
+its existing Connect profile, including token and thinking mappings. Its Chat 
capability
+profile accepts function tools and JSON mode and rejects JSON-schema output, 
custom
+tools and audio output, following the [DeepSeek Chat 
API](https://api-docs.deepseek.com/api/create-chat-completion/).
+
+Provider paths may be relative to a base URL, or the base URL may already 
include the
+complete configured operation path. `/v1` is not appended twice. DeepSeek uses
+`/chat/completions`, including when the configured base URL ends in `/v1` or 
`/beta`.
+Base URLs retain the existing HTTP(S), no-userinfo/query/fragment restriction; 
profile
+query parameters are encoded separately. Header credentials support arbitrary 
header
+names and prefixes (including Bearer); query credentials and unauthenticated 
local
+services are also available. Generated content-type, accept and credential 
headers
+replace conflicting profile headers case-insensitively.
+
+Register another Chat-compatible service as a Spring bean without changing 
Engine,
+Protocol or Transport. For example:
+
+```java
+@Bean
+public AiProxyProvider tenantProvider() {
+    ObjectNode defaults = JsonNodeFactory.instance.objectNode();
+    defaults.put("temperature", 0.7);
+    OpenAiProviderOptions options = new OpenAiProviderOptions(
+            "/generate", OpenAiProviderOptions.Authentication.HEADER, 
"api-key", "",
+            Map.of("X-Tenant", "tenant-a"), Map.of("api-version", "2026-01"),
+            Map.of("client-alias", "upstream-model"), defaults,
+            AiProviderCapabilities.chat(), null, "max_tokens");
+    return new OpenAiCompatible("tenant-provider", options);
+}
+```
+
+Set the selector's `provider` to `tenant-provider`, its `baseUrl` to the 
service base
+URL and its `apiKey` to the credential. The profile also works with a 
collection-based
+`AiProxyProviderFactory` without Spring. Existing custom implementations keep 
their
+interface contract and the `OpenAi(ProviderProfile)` constructor remains 
available.
+
+Model precedence is configured routed model, then client model, then profile 
default.
+Alias mapping applies to the selected model. Defaults fill only absent fields; 
explicit
+client values survive unless a gateway override is configured. Defaults cannot 
replace
+`messages`, `input`, `stream` or internal fallback configuration. Streaming 
selection
+therefore remains consistent with Engine retry/response policy. A Connect 
dialect
+that changes the selected streaming mode is rejected before HTTP execution. 
Profile maps and defaults
+are copied and per-request changes do not mutate the registered profile. 
Profile string
+representations omit headers, query values and body defaults.
+
+Provider response differences remain in the request-bound Connect response 
adapter or
+the existing `AiProxyProvider.adaptResponse` hook. DeepSeek Chat response 
fields, including
+`reasoning_content` and vendor usage fields, remain passthrough for Chat 
clients; Connect
+converts reasoning and usage for Responses clients. Providers do not open 
connections,
+configure pools/timeouts, write client responses, or implement retry/fallback 
policy.
+
 ## Compatibility and limits
 
 - Existing Chat routes retain their protocol identifier, upstream endpoint,
@@ -119,6 +198,7 @@ thinking/tool configurations rather than silently changing 
their meaning.
   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
+change except for OpenAI token-field normalization described above. Responses 
routes
+opt in through the selector handle; DeepSeek routes should review the 
capability and
+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/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/ShenyuAiRequest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/ShenyuAiRequest.java
index b05540257e..4a7ac89499 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/ShenyuAiRequest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/model/ShenyuAiRequest.java
@@ -33,6 +33,8 @@ public final class ShenyuAiRequest {
 
     private String protocol;
 
+    private Operation operation = Operation.CHAT_COMPLETION;
+
     private String model;
 
     private Integer maxTokens;
@@ -68,6 +70,24 @@ public final class ShenyuAiRequest {
         return protocol;
     }
 
+    /**
+     * Get the operation established by the protocol decoder.
+     *
+     * @return requested operation
+     */
+    public Operation getOperation() {
+        return operation;
+    }
+
+    /**
+     * Set the operation established by the protocol decoder.
+     *
+     * @param operation requested operation
+     */
+    public void setOperation(final Operation operation) {
+        this.operation = operation;
+    }
+
     /**
      * Get requested model.
      *
@@ -175,4 +195,11 @@ public final class ShenyuAiRequest {
     public void setMessages(final List<ShenyuAiMessage> messages) {
         this.messages = messages;
     }
+
+    /**
+     * Generation operations supported by the current protocol decoders.
+     */
+    public enum Operation {
+        CHAT_COMPLETION, RESPONSE
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiChat.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiChat.java
index ddd4d8dc72..b8ed66d338 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiChat.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/protocol/OpenAiChat.java
@@ -73,6 +73,7 @@ public final class OpenAiChat implements AiProxyProtocol {
                 new ShenyuAiRequest();
 
         request.setProtocol(NAME);
+        request.setOperation(ShenyuAiRequest.Operation.CHAT_COMPLETION);
         request.setRawBody(rawBody);
         request.setRawPayload(payload.deepCopy());
         request.setModel(readOptionalText(
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
index d578125e7f..ead46e070a 100644
--- 
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
@@ -65,6 +65,7 @@ public final class OpenAiResponses implements AiProxyProtocol 
{
         }
         ShenyuAiRequest request = new ShenyuAiRequest();
         request.setProtocol(NAME);
+        request.setOperation(ShenyuAiRequest.Operation.RESPONSE);
         request.setRawBody(rawBody);
         request.setRawPayload(payload.deepCopy());
         request.setModel(Objects.isNull(model) || model.isNull() ? null : 
model.textValue());
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProviderCapabilities.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProviderCapabilities.java
new file mode 100644
index 0000000000..20d3d1dfe0
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProviderCapabilities.java
@@ -0,0 +1,140 @@
+/*
+ * 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.provider;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiProxyException;
+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 java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Declared provider capabilities, independent of HTTP execution and model 
discovery.
+ *
+ * @param protocols supported client protocols
+ * @param operations supported AI operations
+ * @param features supported request features
+ */
+public record AiProviderCapabilities(Set<String> protocols, 
Set<ShenyuAiRequest.Operation> operations, Set<Feature> features) {
+
+    /**
+     * Copy declarations so request handling never mutates provider 
configuration.
+     */
+    public AiProviderCapabilities {
+        protocols = Set.copyOf(protocols);
+        operations = Set.copyOf(operations);
+        features = Set.copyOf(features);
+    }
+
+    /**
+     * General Chat upstream capabilities, including conversion from Responses 
clients.
+     *
+     * @return general capabilities
+     */
+    public static AiProviderCapabilities chat() {
+        return new AiProviderCapabilities(Set.of(OpenAiChat.NAME, 
OpenAiResponses.NAME),
+                EnumSet.of(ShenyuAiRequest.Operation.CHAT_COMPLETION, 
ShenyuAiRequest.Operation.RESPONSE),
+                EnumSet.of(Feature.STREAMING, Feature.TOOL_CALLING, 
Feature.CUSTOM_TOOLS, Feature.JSON_MODE,
+                        Feature.JSON_SCHEMA, Feature.STREAM_USAGE, 
Feature.AUDIO_OUTPUT));
+    }
+
+    /**
+     * Validate the decoded request and effective gateway streaming setting.
+     *
+     * @param request decoded request
+     * @param stream effective streaming setting
+     */
+    public void validate(final ShenyuAiRequest request, final boolean stream) {
+        if (!protocols.contains(request.getProtocol())) {
+            reject("protocol " + request.getProtocol());
+        }
+        if (Objects.isNull(request.getOperation()) || 
!operations.contains(request.getOperation())) {
+            reject("operation " + request.getOperation());
+        }
+        if (stream) {
+            require(Feature.STREAMING);
+        }
+    }
+
+    /**
+     * Validate features in the final Chat payload, after conversion and 
defaults.
+     *
+     * @param payload final provider payload
+     */
+    public void validatePayload(final JsonNode payload) {
+        if (nonempty(payload.get("tools")) || 
nonempty(payload.get("functions"))) {
+            require(Feature.TOOL_CALLING);
+        }
+        for (String field : new String[] {"tool_choice", "function_call"}) {
+            JsonNode choice = payload.get(field);
+            if (Objects.nonNull(choice) && !choice.isNull() && 
!(choice.isTextual() && "none".equals(choice.asText()))) {
+                require(Feature.TOOL_CALLING);
+            }
+        }
+        JsonNode tools = payload.get("tools");
+        if (Objects.nonNull(tools) && tools.isArray()) {
+            for (JsonNode tool : tools) {
+                if ("custom".equals(tool.path("type").asText())) {
+                    require(Feature.CUSTOM_TOOLS);
+                }
+            }
+        }
+        String format = payload.path("response_format").path("type").asText();
+        if ("json_object".equals(format)) {
+            require(Feature.JSON_MODE);
+        } else if ("json_schema".equals(format)) {
+            require(Feature.JSON_SCHEMA);
+        }
+        if (payload.path("stream_options").path("include_usage").asBoolean()) {
+            require(Feature.STREAM_USAGE);
+        }
+        JsonNode modalities = payload.get("modalities");
+        if (Objects.nonNull(modalities) && modalities.isArray()) {
+            for (JsonNode modality : modalities) {
+                if ("audio".equals(modality.asText())) {
+                    require(Feature.AUDIO_OUTPUT);
+                }
+            }
+        }
+    }
+
+    private boolean nonempty(final JsonNode node) {
+        return Objects.nonNull(node) && !node.isNull() && !node.isEmpty();
+    }
+
+    private void require(final Feature feature) {
+        if (!features.contains(feature)) {
+            reject("feature " + feature);
+        }
+    }
+
+    private void reject(final String capability) {
+        throw new AiProxyException(AiProxyException.Kind.REQUEST, "AI provider 
does not support " + capability, null);
+    }
+
+    /**
+     * Features checked when ShenYu constructs the final upstream request.
+     */
+    public enum Feature {
+        STREAMING, TOOL_CALLING, CUSTOM_TOOLS, JSON_MODE, JSON_SCHEMA, 
STREAM_USAGE, AUDIO_OUTPUT
+    }
+}
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 2f3ee35140..aff2bed098 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
@@ -22,6 +22,8 @@ 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.ShenyuAiRequest;
 
+import java.util.Objects;
+
 /**
  * Contract for adapting a ShenYu AI request to a provider request.
  */
@@ -43,6 +45,16 @@ public interface AiProxyProvider {
      */
     AiUpstreamRequest buildRequest(ShenyuAiRequest request, AiCommonConfig 
config);
 
+    /**
+     * Get provider feature and operation declarations.
+     * Existing custom providers retain the general Chat capability default.
+     *
+     * @return provider capabilities
+     */
+    default AiProviderCapabilities capabilities() {
+        return AiProviderCapabilities.chat();
+    }
+
     /**
      * Check whether the provider supports a client protocol.
      *
@@ -50,7 +62,7 @@ public interface AiProxyProvider {
      * @return whether supported
      */
     default boolean supports(final String protocol) {
-        return true;
+        return Objects.nonNull(protocol) && 
capabilities().protocols().contains(protocol);
     }
 
     /**
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 c41f491a56..ceedf9db2f 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,47 +22,69 @@ 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.AiProxyException;
 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.apache.shenyu.plugin.ai.proxy.enhanced.protocol.OpenAiResponses;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 
 import java.net.URI;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.TreeMap;
+import java.util.regex.Pattern;
 
 /**
- * OpenAI provider adapter.
+ * OpenAI provider adapter; request construction opens no HTTP connections.
  */
 public final class OpenAi implements AiProxyProvider {
 
     public static final String NAME = "openai";
 
-    private static final String CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
+    private static final Pattern HEADER_NAME = 
Pattern.compile("[!#$%&'*+.^_`|~0-9A-Za-z-]+");
 
-    private final ProviderProfile profile;
+    private final OpenAiProviderOptions options;
 
     /**
-     * Create a generic OpenAI Chat upstream without provider-specific 
adaptation.
+     * Create an OpenAI Chat upstream.
      */
     public OpenAi() {
-        this(null);
+        this(OpenAiProviderOptions.openAi());
     }
 
     /**
-     * Create a Chat upstream with an explicitly selected provider dialect.
+     * Retain the explicit Connect dialect constructor for existing extensions.
      *
-     * @param profile provider profile, or null for generic OpenAI 
compatibility
+     * @param profile explicitly selected provider dialect
      */
     public OpenAi(final ProviderProfile profile) {
-        this.profile = profile;
+        this(OpenAiProviderOptions.generic(profile));
+    }
+
+    /**
+     * Create a Chat upstream with explicit provider options.
+     *
+     * @param options provider options
+     */
+    private OpenAi(final OpenAiProviderOptions options) {
+        this.options = Objects.requireNonNull(options, "AI provider options 
are required");
+    }
+
+    /**
+     * Create a provider with explicit options while retaining the legacy 
dialect constructor.
+     *
+     * @param options provider options
+     * @return configured provider
+     */
+    public static OpenAi withOptions(final OpenAiProviderOptions options) {
+        return new OpenAi(options);
     }
 
     @Override
@@ -70,78 +92,95 @@ public final class OpenAi implements AiProxyProvider {
         return NAME;
     }
 
+    @Override
+    public AiProviderCapabilities capabilities() {
+        return options.capabilities();
+    }
+
     @Override
     public AiUpstreamRequest buildRequest(final ShenyuAiRequest request, final 
AiCommonConfig config) {
         validateConfig(config);
         if (!supports(request.getProtocol())) {
-            throw new IllegalArgumentException(
-                    "Unsupported OpenAI protocol: " + request.getProtocol());
+            throw new IllegalArgumentException("Unsupported OpenAI protocol: " 
+ request.getProtocol());
         }
-
-        final ObjectNode payload = request.getRawPayload().deepCopy();
-        boolean changed = payload.has("fallbackConfig");
+        boolean stream = request.hasStream() ? request.isStream() : 
Boolean.TRUE.equals(config.getStream());
+        capabilities().validate(request, stream);
+        ObjectNode payload = request.getRawPayload().deepCopy();
         payload.remove("fallbackConfig");
-
+        options.defaults().fields().forEachRemaining(entry -> {
+            if (!payload.has(entry.getKey())) {
+                payload.set(entry.getKey(), entry.getValue());
+            }
+        });
+        String configuredModel = config.getModel();
+        String model = Objects.nonNull(configuredModel) && 
!configuredModel.isBlank() ? configuredModel : 
payload.path("model").asText(null);
+        String mappedModel = Objects.isNull(model) ? null : 
options.modelMappings().getOrDefault(model, model);
+        if (Objects.nonNull(mappedModel)) {
+            payload.put("model", mappedModel);
+        }
         if (Objects.nonNull(config.getTemperature())) {
             payload.put("temperature", config.getTemperature());
-            changed = true;
         }
         if (Objects.nonNull(config.getMaxTokens())) {
+            if (config.getMaxTokens() <= 0) {
+                throw new IllegalArgumentException("AI maxTokens must be 
positive");
+            }
             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());
+                String field = options.maxTokensField().isEmpty()
+                        ? (payload.hasNonNull("max_completion_tokens") ? 
"max_completion_tokens" : "max_tokens") : options.maxTokensField();
+                payload.remove("max_tokens");
+                payload.remove("max_completion_tokens");
+                payload.put(field, config.getMaxTokens());
             }
-            changed = true;
         }
-
-        final boolean stream = request.hasStream()
-                ? request.isStream()
-                : Boolean.TRUE.equals(config.getStream());
         if (!request.hasStream() && Objects.nonNull(config.getStream())) {
             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(converted) || changed;
-        }
-
-        final Map<String, List<String>> headers =
-                new LinkedHashMap<>();
-        headers.put(
-                HttpHeaders.AUTHORIZATION,
-                List.of("Bearer " + config.getApiKey()));
-        headers.put(
-                HttpHeaders.CONTENT_TYPE,
-                List.of(MediaType.APPLICATION_JSON_VALUE));
-        headers.put(
-                HttpHeaders.ACCEPT,
-                List.of(stream
-                        ? MediaType.TEXT_EVENT_STREAM_VALUE
-                        : MediaType.APPLICATION_JSON_VALUE));
-
-        final AiUpstreamRequest upstreamRequest =
-                new AiUpstreamRequest();
-        upstreamRequest.setMethod(HttpMethod.POST.name());
-        upstreamRequest.setUri(resolveEndpoint(
-                config.getBaseUrl(),
-                CHAT_COMPLETIONS_PATH));
-        upstreamRequest.setHeaders(headers);
-        final String body = changed ? converted.toString() : 
request.getRawBody();
-        upstreamRequest.setBody(body.getBytes(StandardCharsets.UTF_8));
-        upstreamRequest.setResponseAdapter(response -> 
adapter.convertResponse(response, stream));
-
-        return upstreamRequest;
+        }
+        ConnectProtocolAdapter adapter = new 
ConnectProtocolAdapter(request.getProtocol(), mappedModel, 
options.connectProfile());
+        ObjectNode converted = adapter.convertRequest(payload);
+        JsonNode convertedStream = converted.get("stream");
+        if (Objects.nonNull(convertedStream) && !convertedStream.isNull() && 
!convertedStream.isBoolean()
+                || converted.path("stream").asBoolean() != stream) {
+            throw new AiProxyException(AiProxyException.Kind.REQUEST, "AI 
provider dialect must preserve the selected streaming mode", null);
+        }
+        if (stream && 
capabilities().features().contains(AiProviderCapabilities.Feature.STREAM_USAGE))
 {
+            enableStreamUsage(converted);
+        }
+        capabilities().validatePayload(converted);
+        AiUpstreamRequest upstream = new AiUpstreamRequest();
+        upstream.setMethod(HttpMethod.POST.name());
+        upstream.setUri(resolveEndpoint(config));
+        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;
+    }
+
+    private Map<String, List<String>> headers(final AiCommonConfig config, 
final boolean stream) {
+        Map<String, List<String>> headers = new LinkedHashMap<>();
+        options.headers().forEach((name, value) -> putHeader(headers, name, 
value));
+        putHeader(headers, HttpHeaders.CONTENT_TYPE, 
MediaType.APPLICATION_JSON_VALUE);
+        putHeader(headers, HttpHeaders.ACCEPT, stream ? 
MediaType.TEXT_EVENT_STREAM_VALUE : MediaType.APPLICATION_JSON_VALUE);
+        if (options.authentication() == 
OpenAiProviderOptions.Authentication.HEADER) {
+            putHeader(headers, options.credentialName(), 
options.credentialPrefix() + config.getApiKey());
+        }
+        return headers;
+    }
+
+    private void putHeader(final Map<String, List<String>> headers, final 
String name, final String value) {
+        if (!HEADER_NAME.matcher(name).matches() || value.contains("\r") || 
value.contains("\n")) {
+            throw new IllegalArgumentException("Invalid AI provider header: " 
+ name);
+        }
+        headers.keySet().removeIf(existing -> existing.equalsIgnoreCase(name));
+        headers.put(name, List.of(value));
     }
 
-    private boolean enableStreamUsage(final ObjectNode payload) {
-        final JsonNode current = payload.get("stream_options");
-        final ObjectNode streamOptions;
+    private void enableStreamUsage(final ObjectNode payload) {
+        JsonNode current = payload.get("stream_options");
+        ObjectNode streamOptions;
         if (Objects.isNull(current) || current.isNull()) {
             streamOptions = payload.putObject("stream_options");
         } else if (current.isObject()) {
@@ -151,39 +190,57 @@ public final class OpenAi implements AiProxyProvider {
         }
         if (!streamOptions.has("include_usage")) {
             streamOptions.put("include_usage", true);
-            return true;
         }
-        return false;
     }
 
     private void validateConfig(final AiCommonConfig config) {
+        Objects.requireNonNull(config, "AI provider configuration is 
required");
         if (Objects.isNull(config.getBaseUrl()) || 
config.getBaseUrl().isBlank()) {
-            throw new IllegalArgumentException("OpenAI baseUrl must not be 
empty");
+            throw new IllegalArgumentException("AI baseUrl must not be empty");
         }
-        if (Objects.isNull(config.getApiKey()) || 
config.getApiKey().isBlank()) {
-            throw new IllegalArgumentException("OpenAI apiKey must not be 
empty");
+        if (options.authentication() != 
OpenAiProviderOptions.Authentication.NONE
+                && (Objects.isNull(config.getApiKey()) || 
config.getApiKey().isBlank())) {
+            throw new IllegalArgumentException("AI apiKey must not be empty");
         }
     }
 
-    private URI resolveEndpoint(
-            final String baseUrl,
-            final String path) {
-
-        final String normalizedBaseUrl =
-                baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
-        final String relativePath =
-                path.startsWith("/") ? path.substring(1) : path;
-
-        final URI base = URI.create(normalizedBaseUrl);
+    private URI resolveEndpoint(final AiCommonConfig config) {
+        URI base = URI.create(config.getBaseUrl());
         if (!("http".equalsIgnoreCase(base.getScheme()) || 
"https".equalsIgnoreCase(base.getScheme()))
                 || Objects.isNull(base.getHost()) || 
Objects.nonNull(base.getRawQuery()) || Objects.nonNull(base.getRawFragment()) 
|| Objects.nonNull(base.getRawUserInfo())) {
             throw new IllegalArgumentException("AI baseUrl must be an HTTP(S) 
endpoint without credentials, query or fragment");
         }
-        return base.resolve(base.getPath().endsWith("/v1/") ? 
relativePath.substring(3) : relativePath);
+        String path = options.path().startsWith("/") ? 
options.path().substring(1) : options.path();
+        String basePath = base.getPath().endsWith("/") ? 
base.getPath().substring(0, base.getPath().length() - 1) : base.getPath();
+        URI endpoint;
+        if (basePath.endsWith("/" + path)) {
+            endpoint = URI.create(base.toString().replaceAll("/+$", ""));
+        } else {
+            if (basePath.endsWith("/v1") && path.startsWith("v1/")) {
+                path = path.substring(3);
+            }
+            URI directory = URI.create(base.toString().endsWith("/") ? 
base.toString() : base + "/");
+            endpoint = directory.resolve(path);
+        }
+        Map<String, String> query = new TreeMap<>(options.queryParameters());
+        if (options.authentication() == 
OpenAiProviderOptions.Authentication.QUERY) {
+            query.put(options.credentialName(), options.credentialPrefix() + 
config.getApiKey());
+        }
+        if (query.isEmpty()) {
+            return endpoint;
+        }
+        StringBuilder uri = new StringBuilder(endpoint.toString()).append('?');
+        query.forEach((name, value) -> 
uri.append(encode(name)).append('=').append(encode(value)).append('&'));
+        uri.setLength(uri.length() - 1);
+        return URI.create(uri.toString());
+    }
+
+    private String encode(final String value) {
+        return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", 
"%20");
     }
 
     @Override
     public boolean supports(final String protocol) {
-        return OpenAiChat.NAME.equals(protocol) || 
OpenAiResponses.NAME.equals(protocol);
+        return Objects.nonNull(protocol) && 
capabilities().protocols().contains(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 06daf73886..e86964630c 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,11 +17,12 @@
 
 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;
 
+import java.util.Objects;
+
 /**
  * Named OpenAI-compatible profile sharing endpoint and request adaptation.
  */
@@ -32,9 +33,26 @@ public final class OpenAiCompatible implements 
AiProxyProvider {
     private final OpenAi delegate;
 
     public OpenAiCompatible(final String name) {
+        this(name, Objects.nonNull(name) && 
"DeepSeek".equalsIgnoreCase(name.trim().replace("-", "").replace("_", "")) ? 
OpenAiProviderOptions.deepSeek() : OpenAiProviderOptions.generic(null));
+    }
+
+    /**
+     * Register another compatible service without changing Engine, Protocol 
or Transport.
+     *
+     * @param name provider identifier
+     * @param options explicitly selected provider options
+     */
+    public OpenAiCompatible(final String name, final OpenAiProviderOptions 
options) {
+        if (Objects.isNull(name) || name.isBlank()) {
+            throw new IllegalArgumentException("AI provider name must not be 
blank");
+        }
         this.name = name;
-        // Only an explicitly selected DeepSeek provider enables its dialect 
rules.
-        this.delegate = new OpenAi("DeepSeek".equalsIgnoreCase(name) ? 
ProviderProfile.deepSeekChat() : null);
+        this.delegate = OpenAi.withOptions(options);
+    }
+
+    @Override
+    public AiProviderCapabilities capabilities() {
+        return delegate.capabilities();
     }
 
     @Override
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptions.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptions.java
new file mode 100644
index 0000000000..1c6e29382d
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptions.java
@@ -0,0 +1,179 @@
+/*
+ * 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.provider;
+
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.github.agenticai.connect.ProviderProfile;
+
+import java.net.URI;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Immutable configuration for a named Chat-compatible provider in the 
explicit registry.
+ *
+ * @param path relative upstream operation path
+ * @param authentication credential placement
+ * @param credentialName credential header or query parameter name
+ * @param credentialPrefix credential prefix
+ * @param headers provider headers
+ * @param queryParameters provider query parameters
+ * @param modelMappings client or routed model aliases
+ * @param defaults missing-field defaults in the client protocol's payload
+ * @param capabilities provider capability declarations
+ * @param dialect Connect provider dialect, or null for identity adaptation
+ * @param maxTokensField Chat token field, or empty to preserve the client's 
field choice
+ */
+public record OpenAiProviderOptions(String path, Authentication 
authentication, String credentialName, String credentialPrefix,
+                                    Map<String, String> headers, Map<String, 
String> queryParameters, Map<String, String> modelMappings,
+                                    ObjectNode defaults, 
AiProviderCapabilities capabilities, ProviderProfile dialect, String 
maxTokensField) {
+
+    /**
+     * Copy mutable data and reject options that would change the protocol or 
transport contract.
+     */
+    public OpenAiProviderOptions {
+        URI relative = URI.create(path);
+        if (path.isBlank() || relative.isAbsolute() || path.startsWith("//") 
|| Objects.nonNull(relative.getRawQuery())
+                || Objects.nonNull(relative.getRawFragment())) {
+            throw new IllegalArgumentException("AI provider path must be a 
relative path without query or fragment");
+        }
+        Objects.requireNonNull(authentication, "AI provider authentication is 
required");
+        Objects.requireNonNull(credentialName, "AI provider credential name is 
required");
+        Objects.requireNonNull(credentialPrefix, "AI provider credential 
prefix is required");
+        Objects.requireNonNull(capabilities, "AI provider capabilities are 
required");
+        if (authentication != Authentication.NONE && credentialName.isBlank()) 
{
+            throw new IllegalArgumentException("AI provider credential name 
must not be blank");
+        }
+        headers = Map.copyOf(headers);
+        queryParameters = Map.copyOf(queryParameters);
+        modelMappings = Map.copyOf(modelMappings);
+        defaults = defaults.deepCopy();
+        for (String field : new String[] {"messages", "input", "stream", 
"fallbackConfig"}) {
+            if (defaults.has(field)) {
+                throw new IllegalArgumentException("AI provider defaults must 
not replace the request envelope: " + field);
+            }
+        }
+        if (!"".equals(maxTokensField) && !"max_tokens".equals(maxTokensField) 
&& !"max_completion_tokens".equals(maxTokensField)) {
+            throw new IllegalArgumentException("AI provider token field must 
be max_tokens or max_completion_tokens");
+        }
+    }
+
+    /**
+     * OpenAI options use the current Chat token-limit field.
+     *
+     * @return OpenAI options
+     */
+    public static OpenAiProviderOptions openAi() {
+        return generic(null).withMaxTokensField("max_completion_tokens");
+    }
+
+    /**
+     * Preserve generic compatible payloads unless a dialect is explicitly 
supplied.
+     *
+     * @param dialect explicitly selected Connect dialect
+     * @return compatible options
+     */
+    public static OpenAiProviderOptions generic(final ProviderProfile dialect) 
{
+        return new OpenAiProviderOptions("/v1/chat/completions", 
Authentication.HEADER, "Authorization", "Bearer ",
+                Map.of(), Map.of(), Map.of(), 
JsonNodeFactory.instance.objectNode(), AiProviderCapabilities.chat(), dialect, 
"");
+    }
+
+    /**
+     * DeepSeek Chat supports function tools and JSON mode, with its Connect 
dialect.
+     *
+     * @return DeepSeek options
+     */
+    public static OpenAiProviderOptions deepSeek() {
+        EnumSet<AiProviderCapabilities.Feature> features = 
EnumSet.of(AiProviderCapabilities.Feature.STREAMING,
+                AiProviderCapabilities.Feature.TOOL_CALLING, 
AiProviderCapabilities.Feature.JSON_MODE, 
AiProviderCapabilities.Feature.STREAM_USAGE);
+        AiProviderCapabilities all = AiProviderCapabilities.chat();
+        return new OpenAiProviderOptions("/chat/completions", 
Authentication.HEADER, "Authorization", "Bearer ",
+                Map.of(), Map.of(), Map.of(), 
JsonNodeFactory.instance.objectNode(),
+                new AiProviderCapabilities(all.protocols(), all.operations(), 
features), ProviderProfile.deepSeekChat(), "");
+    }
+
+    /**
+     * Get a defensive copy of provider defaults.
+     *
+     * @return provider defaults
+     */
+    @Override
+    public ObjectNode defaults() {
+        return defaults.deepCopy();
+    }
+
+    /**
+     * Select one Chat token-limit field, implemented in Connect's provider 
adaptation phase.
+     *
+     * @param field Chat token-limit field
+     * @return updated options
+     */
+    public OpenAiProviderOptions withMaxTokensField(final String field) {
+        return new OpenAiProviderOptions(path, authentication, credentialName, 
credentialPrefix, headers, queryParameters,
+                modelMappings, defaults, capabilities, dialect, field);
+    }
+
+    /**
+     * Get the request-scoped Connect dialect including the explicit 
token-field choice.
+     *
+     * @return provider dialect
+     */
+    public ProviderProfile connectProfile() {
+        if (maxTokensField.isEmpty()) {
+            return dialect;
+        }
+        return new ProviderProfile() {
+            @Override
+            public void adapt(final JsonObject payload, final Adaptation 
adaptation) {
+                if (Objects.nonNull(dialect)) {
+                    dialect.adapt(payload, adaptation);
+                }
+                JsonElement tokens = payload.get("max_completion_tokens");
+                if (Objects.isNull(tokens) || tokens.isJsonNull()) {
+                    tokens = payload.get("max_tokens");
+                }
+                if (Objects.nonNull(tokens)) {
+                    payload.remove("max_tokens");
+                    payload.remove("max_completion_tokens");
+                    payload.add(maxTokensField, tokens);
+                }
+            }
+
+            @Override
+            public boolean rendersReasoning() {
+                return Objects.nonNull(dialect) && dialect.rendersReasoning();
+            }
+        };
+    }
+
+    @Override
+    public String toString() {
+        return "OpenAiProviderOptions[path=" + path + ", authentication=" + 
authentication + ", capabilities=" + capabilities + "]";
+    }
+
+    /**
+     * Credentials can be placed in a header or query parameter, or omitted.
+     */
+    public enum Authentication {
+        HEADER, QUERY, NONE
+    }
+}
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 42a0811487..e146623e06 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
@@ -182,9 +182,10 @@ public final class AiProxyEngine {
         if (!provider.supports(protocol.name())) {
             throw new ShenyuException("Unsupported AI protocol/provider 
combination");
         }
+        boolean stream = decoded.hasStream() ? decoded.isStream() : 
Boolean.TRUE.equals(config.getStream());
+        provider.capabilities().validate(decoded, stream);
         try {
             AiUpstreamRequest request = provider.buildRequest(decoded, config);
-            boolean stream = decoded.hasStream() ? decoded.isStream() : 
Boolean.TRUE.equals(config.getStream());
             return new PreparedExchange(protocol, provider, request, stream);
         } catch (IllegalArgumentException error) {
             throw new AiProxyException(AiProxyException.Kind.PROVIDER, 
"Invalid AI provider configuration", error);
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderHttpTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderHttpTest.java
new file mode 100644
index 0000000000..7453e95ee6
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/AiProxyProviderHttpTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.provider;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+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.service.AiProxyEngine;
+import 
org.apache.shenyu.plugin.ai.proxy.enhanced.transport.WebClientAiProxyTransport;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.netty.DisposableServer;
+import reactor.netty.http.server.HttpServer;
+import reactor.test.StepVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Providers construct requests; the existing engine and transport execute 
real HTTP.
+ */
+class AiProxyProviderHttpTest {
+
+    private static final Duration TIMEOUT = Duration.ofSeconds(5);
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Test
+    void customProfileAppliesHeadersQueryModelsAndTokensThroughRealTransport() 
throws Exception {
+        AtomicReference<String> forwarded = new AtomicReference<>();
+        String reply = "{\"choices\":[],\"provider_extra\":{\"kept\":true}}";
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) -> {
+            assertEquals("/tenant/generate?api-version=2026-01", 
request.uri());
+            assertEquals("gateway-key", 
request.requestHeaders().get("api-key"));
+            assertEquals("tenant", request.requestHeaders().get("X-Tenant"));
+            return request.receive().aggregate().asString().flatMap(body -> {
+                forwarded.set(body);
+                return response.header("Content-Type", 
"application/json").header("X-Provider", 
"custom").sendString(Flux.just(reply)).then();
+            });
+        }).bindNow(TIMEOUT);
+        try {
+            OpenAiProviderOptions options = new 
OpenAiProviderOptions("/generate", OpenAiProviderOptions.Authentication.HEADER, 
"api-key", "",
+                    Map.of("X-Tenant", "tenant"), Map.of("api-version", 
"2026-01"), Map.of("alias", "native-model"),
+                    MAPPER.createObjectNode(), AiProviderCapabilities.chat(), 
null, "max_tokens");
+            OpenAiCompatible provider = new OpenAiCompatible("custom", 
options);
+            AiProxyEngine engine = engine(provider);
+            AiCommonConfig config = config(server, "custom", OpenAiChat.NAME);
+            config.setBaseUrl(config.getBaseUrl() + "/tenant");
+            config.setMaxTokens(21);
+            String body = 
"{\"model\":\"alias\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"vendor_field\":true}";
+            StepVerifier.create(engine.execute(body, config, Optional.empty(), 
response -> {
+                assertEquals(List.of("custom"), 
response.getHeaders().get("X-Provider"));
+                return response.getBody().map(bytes -> new String(bytes, 
StandardCharsets.UTF_8)).reduce("", String::concat)
+                        .doOnNext(received -> assertEquals(reply, 
received)).then();
+            })).verifyComplete();
+            JsonNode payload = MAPPER.readTree(forwarded.get());
+            assertEquals("native-model", payload.get("model").asText());
+            assertEquals(21, payload.get("max_tokens").asInt());
+            assertFalse(payload.has("max_completion_tokens"));
+            assertTrue(payload.get("vendor_field").asBoolean());
+        } finally {
+            server.disposeNow(TIMEOUT);
+        }
+    }
+
+    @Test
+    void deepSeekChatStreamConvertsReasoningAndUsageForResponsesClients() 
throws Exception {
+        AtomicReference<String> forwarded = new AtomicReference<>();
+        AtomicReference<String> received = new AtomicReference<>();
+        String reply = "data: 
{\"id\":\"d\",\"model\":\"deepseek-flash\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"trace\"}}]}\n\n"
+                + "data: 
{\"id\":\"d\",\"model\":\"deepseek-flash\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"answer\"}}]}\n\n"
+                + "data: 
{\"id\":\"d\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],"
+                + 
"\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":5,\"total_tokens\":8}}\n\ndata:
 [DONE]\n\n";
+        DisposableServer server = 
HttpServer.create().host("127.0.0.1").port(0).handle((request, response) -> {
+            assertEquals("/chat/completions", request.uri());
+            assertEquals("Bearer gateway-key", 
request.requestHeaders().get("Authorization"));
+            return request.receive().aggregate().asString().flatMap(body -> {
+                forwarded.set(body);
+                return response.header("Content-Type", 
"text/event-stream").sendString(Flux.just(reply)).then();
+            });
+        }).bindNow(TIMEOUT);
+        try {
+            AiProxyEngine engine = engine(new OpenAiCompatible("DeepSeek"));
+            AiCommonConfig config = config(server, "DeepSeek", 
OpenAiResponses.NAME);
+            config.setModel("deepseek-flash");
+            String body = 
"{\"input\":\"hi\",\"stream\":true,\"max_output_tokens\":20,\"reasoning\":{\"effort\":\"medium\"}}";
+            StepVerifier.create(engine.execute(body, config, Optional.empty(), 
response -> response.getBody()
+                    .map(bytes -> new String(bytes, 
StandardCharsets.UTF_8)).reduce("", 
String::concat).doOnNext(received::set).then())).verifyComplete();
+            JsonNode payload = MAPPER.readTree(forwarded.get());
+            assertEquals(20, payload.get("max_tokens").asInt());
+            assertFalse(payload.has("max_completion_tokens"));
+            assertEquals("high", payload.get("reasoning_effort").asText());
+            assertTrue(payload.get("stream").asBoolean());
+            assertTrue(received.get().contains("trace"));
+            assertTrue(received.get().contains("answer"));
+            assertTrue(received.get().contains("event: response.completed"));
+            assertTrue(received.get().contains("\"total_tokens\":8"));
+        } finally {
+            server.disposeNow(TIMEOUT);
+        }
+    }
+
+    private AiProxyEngine engine(final AiProxyProvider provider) {
+        return new AiProxyEngine(new AiProxyProtocolFactory(List.of(new 
OpenAiChat(), new OpenAiResponses())),
+                new AiProxyProviderFactory(List.of(provider)), new 
WebClientAiProxyTransport(WebClient.builder().build()));
+    }
+
+    private AiCommonConfig config(final DisposableServer server, final String 
provider, final String protocol) {
+        AiCommonConfig config = new AiCommonConfig();
+        config.setProvider(provider);
+        config.setProtocol(protocol);
+        config.setBaseUrl("http://127.0.0.1:"; + server.port());
+        config.setApiKey("gateway-key");
+        config.setStream(false);
+        return config;
+    }
+}
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
new file mode 100644
index 0000000000..6b349ed8d9
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/provider/OpenAiProviderOptionsTest.java
@@ -0,0 +1,338 @@
+/*
+ * 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.provider;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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.ShenyuAiRequest;
+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.service.AiProxyEngine;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class OpenAiProviderOptionsTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final String CHAT = "{ \"model\": \"client-alias\", 
\"messages\": [{\"role\":\"user\",\"content\":\"hello\"}], \"stream\": false, 
\"vendor_extra\": {\"nested\":[1,2]} }";
+
+    private AiCommonConfig config;
+
+    @BeforeEach
+    void setUp() {
+        config = new AiCommonConfig();
+        config.setBaseUrl("https://example.org/prefix";);
+        config.setApiKey("test-key");
+        config.setStream(null);
+    }
+
+    @Test
+    void genericCompatibleProfilePreservesUnknownFieldsAndOriginalBytes() 
throws Exception {
+        AiUpstreamRequest result = new 
OpenAiCompatible("custom").buildRequest(chat(CHAT), config);
+        assertEquals(CHAT, text(result.getBody()));
+        assertEquals("Bearer test-key", 
result.getHeaders().get("Authorization").get(0));
+        assertEquals("https://example.org/prefix/v1/chat/completions";, 
result.getUri().toString());
+        assertNotNull(new OpenAi(null));
+    }
+
+    @Test
+    void resolvesRootVersionedPrefixedAndCompleteEndpoints() throws Exception {
+        OpenAi provider = new OpenAi();
+        Map<String, String> endpoints = Map.of(
+                "https://example.org";, 
"https://example.org/v1/chat/completions";,
+                "https://example.org/v1/";, 
"https://example.org/v1/chat/completions";,
+                "https://example.org/prefix/v1";, 
"https://example.org/prefix/v1/chat/completions";,
+                "https://example.org/prefix/v1/chat/completions/";, 
"https://example.org/prefix/v1/chat/completions";,
+                "https://example.org/prefix%20one";, 
"https://example.org/prefix%20one/v1/chat/completions";);
+        for (Map.Entry<String, String> entry : endpoints.entrySet()) {
+            config.setBaseUrl(entry.getKey());
+            assertEquals(entry.getValue(), provider.buildRequest(chat(CHAT), 
config).getUri().toString());
+        }
+    }
+
+    @Test
+    void resolvesCustomPathHeaderAuthenticationAndEncodedQueryParameters() 
throws Exception {
+        OpenAiProviderOptions options = options("/chat/generate", 
OpenAiProviderOptions.Authentication.HEADER, "api-key", "",
+                Map.of("X-Tenant", "tenant", "api-key", "stale", "accept", 
"stale"), Map.of("version", "2026-01", "tag", "a &b/#"), Map.of(), object("{}"),
+                AiProviderCapabilities.chat(), "");
+        AiUpstreamRequest result = new OpenAiCompatible("custom", 
options).buildRequest(chat(CHAT), config);
+        
assertEquals("https://example.org/prefix/chat/generate?tag=a%20%26b%2F%23&version=2026-01";,
 result.getUri().toString());
+        assertEquals(List.of("test-key"), result.getHeaders().get("api-key"));
+        assertEquals(List.of("tenant"), result.getHeaders().get("X-Tenant"));
+        assertEquals(List.of("application/json"), 
result.getHeaders().get("Accept"));
+        assertFalse(result.getHeaders().containsKey("Authorization"));
+        assertFalse(result.getHeaders().containsKey("accept"));
+    }
+
+    @Test
+    void queryCredentialsReplaceConfiguredValueAndAreEncodedOnce() throws 
Exception {
+        config.setApiKey("a+b &/#%");
+        OpenAiProviderOptions options = options("/generate", 
OpenAiProviderOptions.Authentication.QUERY, "key", "",
+                Map.of(), Map.of("key", "stale"), Map.of(), object("{}"), 
AiProviderCapabilities.chat(), "");
+        AiUpstreamRequest result = new OpenAiCompatible("custom", 
options).buildRequest(chat(CHAT), config);
+        
assertEquals("https://example.org/prefix/generate?key=a%2Bb%20%26%2F%23%25";, 
result.getUri().toString());
+        assertFalse(result.getHeaders().containsKey("Authorization"));
+    }
+
+    @Test
+    void unauthenticatedLocalProvidersDoNotRequireApiKeys() throws Exception {
+        config.setApiKey(null);
+        OpenAiProviderOptions options = options("/generate", 
OpenAiProviderOptions.Authentication.NONE, "", "",
+                Map.of(), Map.of(), Map.of(), object("{}"), 
AiProviderCapabilities.chat(), "");
+        assertFalse(new OpenAiCompatible("local", 
options).buildRequest(chat(CHAT), 
config).getHeaders().containsKey("Authorization"));
+        assertThrows(IllegalArgumentException.class, () -> new 
OpenAi().buildRequest(chat(CHAT), config));
+    }
+
+    @Test
+    void modelAliasesAndDefaultsRespectClientValuesAndGatewayOverrides() 
throws Exception {
+        OpenAiProviderOptions options = options("/generate", 
OpenAiProviderOptions.Authentication.HEADER, "Authorization", "Bearer ",
+                Map.of(), Map.of(), Map.of("client-alias", "native-client", 
"route-alias", "native-route"),
+                
object("{\"model\":\"client-alias\",\"temperature\":0.7,\"vendor_default\":{\"x\":1}}"),
 AiProviderCapabilities.chat(), "");
+        OpenAiCompatible provider = new OpenAiCompatible("custom", options);
+        JsonNode first = MAPPER.readTree(provider.buildRequest(chat(CHAT), 
config).getBody());
+        assertEquals("native-client", first.get("model").asText());
+        assertEquals(0.7, first.get("temperature").asDouble());
+        assertEquals(1, first.at("/vendor_default/x").asInt());
+        assertEquals(object(CHAT).get("vendor_extra"), 
first.get("vendor_extra"));
+        config.setModel("route-alias");
+        config.setTemperature(0.2);
+        JsonNode routed = MAPPER.readTree(provider.buildRequest(chat(CHAT), 
config).getBody());
+        assertEquals("native-route", routed.get("model").asText());
+        assertEquals(0.2, routed.get("temperature").asDouble());
+        config.setModel(null);
+        config.setTemperature(null);
+        JsonNode absent = 
MAPPER.readTree(provider.buildRequest(chat("{\"messages\":[{\"role\":\"user\"}],\"temperature\":0.4}"),
 config).getBody());
+        assertEquals("native-client", absent.get("model").asText());
+        assertEquals(0.4, absent.get("temperature").asDouble());
+    }
+
+    @Test
+    void optionsAreImmutableAndDoNotRenderHeaderQueryOrBodySecrets() throws 
Exception {
+        Map<String, String> headers = new LinkedHashMap<>(Map.of("X-Secret", 
"header-secret"));
+        ObjectNode defaults = object("{\"secret\":\"body-secret\"}");
+        OpenAiProviderOptions options = options("/generate", 
OpenAiProviderOptions.Authentication.HEADER, "Authorization", "Bearer ",
+                headers, Map.of("secret", "query-secret"), Map.of(), defaults, 
AiProviderCapabilities.chat(), "");
+        headers.put("X-Secret", "changed");
+        defaults.put("secret", "changed");
+        options.defaults().put("secret", "changed");
+        assertEquals("header-secret", options.headers().get("X-Secret"));
+        assertEquals("body-secret", options.defaults().get("secret").asText());
+        assertThrows(UnsupportedOperationException.class, () -> 
options.headers().put("new", "value"));
+        assertFalse(options.toString().contains("header-secret"));
+        assertFalse(options.toString().contains("query-secret"));
+        assertFalse(options.toString().contains("body-secret"));
+    }
+
+    @Test
+    void openAiMapsLegacyTokensAndOverrideToOneField() throws Exception {
+        String body = CHAT.substring(0, CHAT.length() - 1) + 
",\"max_tokens\":10,\"max_completion_tokens\":20}";
+        JsonNode first = MAPPER.readTree(new OpenAi().buildRequest(chat(body), 
config).getBody());
+        assertEquals(20, first.get("max_completion_tokens").asInt());
+        assertFalse(first.has("max_tokens"));
+        config.setMaxTokens(32);
+        JsonNode overridden = MAPPER.readTree(new 
OpenAi().buildRequest(chat(body), config).getBody());
+        assertEquals(32, overridden.get("max_completion_tokens").asInt());
+        assertFalse(overridden.has("max_tokens"));
+        assertEquals(object(CHAT).get("vendor_extra"), 
overridden.get("vendor_extra"));
+        config.setMaxTokens(0);
+        assertThrows(IllegalArgumentException.class, () -> new 
OpenAi().buildRequest(chat(CHAT), config));
+    }
+
+    @Test
+    void nullModernTokenLimitFallsBackToTheLegacyValue() throws Exception {
+        String body = 
"{\"messages\":[{\"role\":\"user\"}],\"max_tokens\":10,\"max_completion_tokens\":null}";
+        JsonNode payload = MAPPER.readTree(new 
OpenAi().buildRequest(chat(body), config).getBody());
+        assertEquals(10, payload.get("max_completion_tokens").asInt());
+        assertFalse(payload.has("max_tokens"));
+    }
+
+    @Test
+    void compatibleTokenFieldChoiceRunsInConnectAfterResponsesConversion() 
throws Exception {
+        OpenAiCompatible provider = new OpenAiCompatible("legacy", 
OpenAiProviderOptions.generic(null).withMaxTokensField("max_tokens"));
+        config.setMaxTokens(17);
+        AiUpstreamRequest upstream = 
provider.buildRequest(responses("{\"model\":\"m\",\"input\":\"hi\",\"max_output_tokens\":7}"),
 config);
+        JsonNode payload = MAPPER.readTree(upstream.getBody());
+        assertEquals(17, payload.get("max_tokens").asInt());
+        assertFalse(payload.has("max_completion_tokens"));
+        assertFalse(payload.has("max_output_tokens"));
+        assertEquals("hi", payload.at("/messages/0/content").asText());
+    }
+
+    @Test
+    void deepSeekUsesItsExplicitDialectAndPreservesVendorFields() throws 
Exception {
+        config.setBaseUrl("https://api.deepseek.com";);
+        OpenAiCompatible provider = new OpenAiCompatible("DeepSeek");
+        String body = 
"{\"model\":\"deepseek-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],"
+                + 
"\"max_completion_tokens\":20,\"reasoning_effort\":\"medium\",\"vendor_extra\":{\"x\":1}}";
+        AiUpstreamRequest upstream = provider.buildRequest(chat(body), config);
+        JsonNode payload = MAPPER.readTree(upstream.getBody());
+        assertEquals("https://api.deepseek.com/chat/completions";, 
upstream.getUri().toString());
+        assertEquals(20, payload.get("max_tokens").asInt());
+        assertFalse(payload.has("max_completion_tokens"));
+        assertEquals("high", payload.get("reasoning_effort").asText());
+        assertEquals("enabled", payload.at("/thinking/type").asText());
+        assertEquals(1, payload.at("/vendor_extra/x").asInt());
+        config.setBaseUrl("https://api.deepseek.com/beta";);
+        assertEquals("https://api.deepseek.com/beta/chat/completions";, 
provider.buildRequest(chat(body), config).getUri().toString());
+        assertThrows(AiProxyException.class, () -> 
provider.buildRequest(chat(body.replace("\"vendor_extra\"", 
"\"tool_choice\":\"required\",\"vendor_extra\"")), config));
+    }
+
+    @Test
+    void deepSeekRejectsUnsupportedStructuredOutputAndPreservesJsonMode() 
throws Exception {
+        OpenAiCompatible provider = new OpenAiCompatible("DeepSeek");
+        String body = 
"{\"model\":\"deepseek-flash\",\"messages\":[{\"role\":\"user\"}],\"response_format\":{\"type\":\"json_schema\"}}";
+        AiProxyException error = assertThrows(AiProxyException.class, () -> 
provider.buildRequest(chat(body), config));
+        assertEquals(AiProxyException.Kind.REQUEST, error.getKind());
+        JsonNode jsonMode = 
MAPPER.readTree(provider.buildRequest(chat(body.replace("json_schema", 
"json_object")), config).getBody());
+        assertEquals("json_object", 
jsonMode.at("/response_format/type").asText());
+    }
+
+    @Test
+    void 
deepSeekResponseDifferencesStayPassthroughForChatAndConvertForResponses() 
throws Exception {
+        OpenAiCompatible provider = new OpenAiCompatible("DeepSeek");
+        String reply = 
"{\"id\":\"chat-d\",\"created\":1,\"object\":\"chat.completion\",\"model\":\"deepseek-flash\","
+                + 
"\"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)));
+        assertEquals("response", converted.get("object").asText());
+        assertTrue(converted.toString().contains("think"));
+        assertTrue(converted.toString().contains("answer"));
+        assertEquals(8, converted.at("/usage/total_tokens").asInt());
+    }
+
+    @Test
+    void unsupportedOperationsFailBeforeTransportWithoutEngineChanges() {
+        AiProviderCapabilities all = AiProviderCapabilities.chat();
+        AiProviderCapabilities restricted = new 
AiProviderCapabilities(all.protocols(), 
Set.of(ShenyuAiRequest.Operation.CHAT_COMPLETION), all.features());
+        OpenAiCompatible custom = new OpenAiCompatible("restricted", 
options("/generate", OpenAiProviderOptions.Authentication.NONE, "", "",
+                Map.of(), Map.of(), Map.of(), MAPPER.createObjectNode(), 
restricted, ""));
+        AtomicInteger calls = new AtomicInteger();
+        AiProxyEngine engine = new AiProxyEngine(new 
AiProxyProtocolFactory(List.of(new OpenAiResponses())),
+                new AiProxyProviderFactory(List.of(custom)), request -> {
+                    calls.incrementAndGet();
+                    return Mono.empty();
+                });
+        config.setProvider("restricted");
+        config.setProtocol(OpenAiResponses.NAME);
+        StepVerifier.create(engine.execute("{\"input\":\"hi\"}", config, 
java.util.Optional.empty(), response -> response.getBody().then()))
+                .expectErrorMatches(error -> error instanceof AiProxyException 
&& ((AiProxyException) error).getKind() == 
AiProxyException.Kind.REQUEST).verify();
+        assertEquals(0, calls.get());
+    }
+
+    @Test
+    void validatesStreamingToolsJsonAndAudioCapabilitiesOnFinalPayload() 
throws Exception {
+        AiProviderCapabilities all = AiProviderCapabilities.chat();
+        AiProviderCapabilities restricted = new 
AiProviderCapabilities(all.protocols(), all.operations(), Set.of());
+        OpenAiCompatible provider = new OpenAiCompatible("restricted", 
options("/generate", OpenAiProviderOptions.Authentication.NONE, "", "",
+                Map.of(), Map.of(), Map.of(), object("{}"), restricted, ""));
+        for (String feature : List.of("\"stream\":true", 
"\"function_call\":{\"name\":\"forced\"}", 
"\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"f\"}}]",
+                "\"response_format\":{\"type\":\"json_object\"}", 
"\"response_format\":{\"type\":\"json_schema\"}", 
"\"modalities\":[\"audio\"]")) {
+            String body = "{\"messages\":[{\"role\":\"user\"}]," + feature + 
"}";
+            assertThrows(AiProxyException.class, () -> 
provider.buildRequest(chat(body), config));
+        }
+        EnumSet<AiProviderCapabilities.Feature> features = 
EnumSet.allOf(AiProviderCapabilities.Feature.class);
+        features.remove(AiProviderCapabilities.Feature.STREAM_USAGE);
+        OpenAiCompatible noUsage = new OpenAiCompatible("no-usage", 
options("/generate", OpenAiProviderOptions.Authentication.NONE, "", "",
+                Map.of(), Map.of(), Map.of(), object("{}"), new 
AiProviderCapabilities(all.protocols(), all.operations(), features), ""));
+        JsonNode payload = 
MAPPER.readTree(noUsage.buildRequest(chat("{\"messages\":[{\"role\":\"user\"}],\"stream\":true}"),
 config).getBody());
+        assertFalse(payload.has("stream_options"));
+        assertThrows(AiProxyException.class, () -> 
noUsage.buildRequest(chat("{\"messages\":[{\"role\":\"user\"}],\"stream\":true,"
+                + "\"stream_options\":{\"include_usage\":true}}"), config));
+    }
+
+    @Test
+    void dialectCannotChangeTheStreamingModeSelectedByTheEngine() throws 
Exception {
+        OpenAiCompatible provider = new OpenAiCompatible("invalid-dialect", 
OpenAiProviderOptions.generic((payload, adaptation) -> 
payload.addProperty("stream", true)));
+        AiProxyException error = assertThrows(AiProxyException.class, () -> 
provider.buildRequest(chat(CHAT), config));
+        assertEquals(AiProxyException.Kind.REQUEST, error.getKind());
+    }
+
+    @Test
+    void rejectsInvalidProviderOptionsEndpointsAndHeaders() throws Exception {
+        for (String base : List.of("ftp://example.org";, 
"https://user:[email protected]";, "https://example.org?secret=value";, 
"https://example.org#fragment";)) {
+            config.setBaseUrl(base);
+            assertThrows(IllegalArgumentException.class, () -> new 
OpenAi().buildRequest(chat(CHAT), config));
+        }
+        config.setBaseUrl("https://example.org";);
+        for (String path : List.of("https://other.org";, "//other.org", 
"/generate?key=x", "/generate#fragment")) {
+            assertThrows(IllegalArgumentException.class, () -> options(path, 
OpenAiProviderOptions.Authentication.NONE, "", "",
+                    Map.of(), Map.of(), Map.of(), object("{}"), 
AiProviderCapabilities.chat(), ""));
+        }
+        assertThrows(IllegalArgumentException.class, () -> 
OpenAiProviderOptions.openAi().withMaxTokensField("invalid"));
+        assertThrows(IllegalArgumentException.class, () -> 
options("/generate", OpenAiProviderOptions.Authentication.NONE, "", "",
+                Map.of(), Map.of(), Map.of(), object("{\"stream\":true}"), 
AiProviderCapabilities.chat(), ""));
+        OpenAiCompatible provider = new OpenAiCompatible("invalid-header", 
options("/generate", OpenAiProviderOptions.Authentication.HEADER, 
"Authorization", "Bearer ",
+                Map.of("X-Header", "bad\r\nvalue"), Map.of(), Map.of(), 
object("{}"), AiProviderCapabilities.chat(), ""));
+        assertThrows(IllegalArgumentException.class, () -> 
provider.buildRequest(chat(CHAT), config));
+    }
+
+    private OpenAiProviderOptions options(final String path, final 
OpenAiProviderOptions.Authentication authentication,
+                                          final String credentialName, final 
String prefix, final Map<String, String> headers,
+                                          final Map<String, String> query, 
final Map<String, String> aliases, final ObjectNode defaults,
+                                          final AiProviderCapabilities 
capabilities, final String tokenField) {
+        return new OpenAiProviderOptions(path, authentication, credentialName, 
prefix, headers, query, aliases, defaults, capabilities, null, tokenField);
+    }
+
+    private ShenyuAiRequest chat(final String body) throws Exception {
+        return new OpenAiChat().decodeRequest(body, object(body));
+    }
+
+    private ShenyuAiRequest responses(final String body) throws Exception {
+        return new OpenAiResponses().decodeRequest(body, object(body));
+    }
+
+    private ObjectNode object(final String json) throws Exception {
+        return (ObjectNode) MAPPER.readTree(json);
+    }
+
+    private byte[] bytes(final String body) {
+        return body.getBytes(StandardCharsets.UTF_8);
+    }
+
+    private String text(final byte[] body) {
+        return new String(body, 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/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 b31d6d5116..b6746a82ca 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
@@ -26,6 +26,7 @@ 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;
@@ -232,6 +233,7 @@ class AiProxyEngineOrchestrationTest {
         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);
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineTest.java
index 7e72ac4012..7efece125c 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/test/java/org/apache/shenyu/plugin/ai/proxy/enhanced/service/AiProxyEngineTest.java
@@ -21,10 +21,15 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 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.ShenyuAiRequest;
 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;
 import org.apache.shenyu.plugin.ai.proxy.enhanced.transport.AiProxyTransport;
@@ -32,9 +37,12 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import reactor.core.publisher.Flux;
@@ -43,6 +51,7 @@ import reactor.test.StepVerifier;
 
 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;
 
@@ -104,7 +113,7 @@ class AiProxyEngineTest {
 
         assertEquals("gpt-4o-mini", payload.get("model").asText());
         assertEquals(256, payload.get("max_completion_tokens").intValue());
-        assertEquals(256, payload.get("max_tokens").intValue());
+        assertFalse(payload.has("max_tokens"));
         assertTrue(payload.get("unknown").booleanValue());
         
assertTrue(payload.get("stream_options").get("include_usage").booleanValue());
         assertEquals("text/event-stream", 
request.getHeaders().get("Accept").get(0));
@@ -130,4 +139,131 @@ class AiProxyEngineTest {
         assertEquals("hello", 
transportedPayload.get("messages").get(0).get("content").asText());
         assertFalse(transportedPayload.get("stream").booleanValue());
     }
+
+    @Test
+    void testCustomProviderSupportsOnlyDeclaredProtocols() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        final AiProxyProvider provider = customProvider(new 
AiProviderCapabilities(Set.of(OpenAiChat.NAME), all.operations(), 
all.features()), new AtomicInteger());
+
+        assertTrue(provider.supports(OpenAiChat.NAME));
+        assertFalse(provider.supports(OpenAiResponses.NAME));
+        assertFalse(provider.supports("unknown"));
+        assertFalse(provider.supports(null));
+    }
+
+    @Test
+    void testCustomProviderProtocolRejectedBeforeBuildAndTransport() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        final AtomicInteger builds = new AtomicInteger();
+        useCustomProvider(new AiProviderCapabilities(Set.of(OpenAiChat.NAME), 
all.operations(), all.features()), builds);
+        config.setProtocol(OpenAiResponses.NAME);
+
+        StepVerifier.create(engine.execute("{\"input\":\"hello\"}", config, 
Optional.empty(), response -> response.getBody().then()))
+                .expectError(ShenyuException.class).verify();
+
+        assertEquals(0, builds.get());
+        assertNull(transportedRequest.get());
+    }
+
+    @Test
+    void testCustomProviderOperationRejectedBeforeBuildAndTransport() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        config.setProtocol(OpenAiResponses.NAME);
+        assertCapabilityRejected("{\"input\":\"hello\"}",
+                new AiProviderCapabilities(all.protocols(), 
Set.of(ShenyuAiRequest.Operation.CHAT_COMPLETION), all.features()), 
"operation");
+    }
+
+    @Test
+    void testCustomProviderRequestStreamRejectedBeforeBuildAndTransport() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        config.setStream(false);
+        
assertCapabilityRejected("{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}],\"stream\":true}",
+                new AiProviderCapabilities(all.protocols(), all.operations(), 
Set.of()), "STREAMING");
+    }
+
+    @Test
+    void testCustomProviderConfiguredStreamRejectedBeforeBuildAndTransport() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        config.setStream(true);
+        
assertCapabilityRejected("{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}",
+                new AiProviderCapabilities(all.protocols(), all.operations(), 
Set.of()), "STREAMING");
+    }
+
+    @Test
+    void testCustomProviderExplicitNonStreamingOverridesConfiguration() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        final AtomicInteger builds = new AtomicInteger();
+        useCustomProvider(new AiProviderCapabilities(all.protocols(), 
all.operations(), Set.of()), builds);
+        config.setStream(true);
+
+        
StepVerifier.create(engine.execute("{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}],\"stream\":false}",
+                config, Optional.empty(), response -> 
response.getBody().then())).verifyComplete();
+
+        assertEquals(1, builds.get());
+        assertEquals("https://api.openai.com/v1/chat/completions";, 
transportedRequest.get().getUri().toString());
+    }
+
+    @Test
+    void testFallbackProviderCapabilitiesCheckedBeforeBuildAndTransport() {
+        final AiProviderCapabilities all = AiProviderCapabilities.chat();
+        final AtomicInteger builds = new AtomicInteger();
+        final AtomicInteger transports = new AtomicInteger();
+        final AiProxyProvider fallback = customProvider(new 
AiProviderCapabilities(all.protocols(), all.operations(), Set.of()), builds);
+        engine = new AiProxyEngine(new AiProxyProtocolFactory(List.of(new 
OpenAiChat())),
+                new AiProxyProviderFactory(List.of(new OpenAi(), fallback)), 
request -> {
+                    transports.incrementAndGet();
+                    return Mono.error(new 
AiProxyException(AiProxyException.Kind.TRANSPORT, "unavailable", null));
+                }, Duration.ZERO);
+        config.setStream(true);
+        final AiCommonConfig fallbackConfig = new AiCommonConfig(config);
+        fallbackConfig.setProvider("custom");
+
+        
StepVerifier.create(engine.execute("{\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}",
+                config, Optional.of(fallbackConfig), response -> 
response.getBody().then()))
+                .expectErrorMatches(error -> error instanceof AiProxyException 
&& ((AiProxyException) error).getKind() == 
AiProxyException.Kind.REQUEST).verify();
+
+        assertEquals(2, transports.get());
+        assertEquals(0, builds.get());
+    }
+
+    private void assertCapabilityRejected(final String body, final 
AiProviderCapabilities capabilities, final String capability) {
+        final AtomicInteger builds = new AtomicInteger();
+        useCustomProvider(capabilities, builds);
+
+        StepVerifier.create(engine.execute(body, config, Optional.empty(), 
response -> response.getBody().then()))
+                .expectErrorMatches(error -> error instanceof AiProxyException 
&& ((AiProxyException) error).getKind() == AiProxyException.Kind.REQUEST
+                        && error.getMessage().contains(capability)).verify();
+
+        assertEquals(0, builds.get());
+        assertNull(transportedRequest.get());
+    }
+
+    private void useCustomProvider(final AiProviderCapabilities capabilities, 
final AtomicInteger builds) {
+        engine = new AiProxyEngine(new AiProxyProtocolFactory(List.of(new 
OpenAiChat(), new OpenAiResponses())),
+                new 
AiProxyProviderFactory(List.of(customProvider(capabilities, builds))), request 
-> {
+                    transportedRequest.set(request);
+                    return Mono.just(upstreamResponse);
+                });
+        config.setProvider("custom");
+    }
+
+    private AiProxyProvider customProvider(final AiProviderCapabilities 
capabilities, final AtomicInteger builds) {
+        return new AiProxyProvider() {
+            @Override
+            public String name() {
+                return "custom";
+            }
+
+            @Override
+            public AiProviderCapabilities capabilities() {
+                return capabilities;
+            }
+
+            @Override
+            public AiUpstreamRequest buildRequest(final ShenyuAiRequest 
request, final AiCommonConfig providerConfig) {
+                builds.incrementAndGet();
+                return new OpenAi().buildRequest(request, providerConfig);
+            }
+        };
+    }
 }
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 07bf852251..79170c723c 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
@@ -183,6 +183,16 @@ public class AiProxyPluginConfiguration {
     }
 
 
+    /**
+     * Create the generic OpenAI-compatible provider profile.
+     *
+     * @return provider profile
+     */
+    @Bean
+    public OpenAiCompatible openAiCompatibleProvider() {
+        return new OpenAiCompatible("OpenAICompatible");
+    }
+
     /**
      * Create the DeepSeek compatible provider profile.
      *
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 9db0b79b02..8ba08de911 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
@@ -18,6 +18,11 @@
 package org.apache.shenyu.springboot.starter.plugin.ai.proxy;
 
 import 
org.apache.shenyu.plugin.ai.common.spring.ai.registry.AiModelFactoryRegistry;
+import org.apache.shenyu.plugin.ai.common.config.AiCommonConfig;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.model.AiUpstreamRequest;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.AiProxyProvider;
+import org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAiCompatible;
+import 
org.apache.shenyu.plugin.ai.proxy.enhanced.provider.OpenAiProviderOptions;
 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;
@@ -54,13 +59,29 @@ class AiProxyPluginConfigurationTest {
                     .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")) {
+            for (String provider : List.of("OpenAI", "DeepSeek", "ALiYun", 
"OpenAPI", "Moonshot", "OpenAICompatible")) {
                 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));
             }
         });
     }
 
+    @Test
+    void 
discoversAnotherCompatibleProviderWithoutEngineProtocolOrTransportChanges() {
+        OpenAiCompatible provider = new OpenAiCompatible("tenant-profile", 
OpenAiProviderOptions.generic(null));
+        new 
ApplicationContextRunner().withUserConfiguration(AiProxyPluginConfiguration.class)
+                .withBean("tenantProvider", AiProxyProvider.class, () -> 
provider).run(context -> {
+                    
org.assertj.core.api.Assertions.assertThat(context).hasNotFailed();
+                    assertSame(provider, 
context.getBean(AiProxyProviderFactory.class).getProvider("TENANT_PROFILE"));
+                    AiCommonConfig config = new AiCommonConfig();
+                    config.setProvider("tenant-profile");
+                    config.setBaseUrl("https://example.org";);
+                    config.setApiKey("key");
+                    AiUpstreamRequest request = 
context.getBean(AiProxyEngine.class).buildRequest("{\"messages\":[{\"role\":\"user\"}]}",
 config);
+                    assertEquals("https://example.org/v1/chat/completions";, 
request.getUri().toString());
+                });
+    }
+
     @Test
     void allowsACustomTransportAndHonorsTheEnableFlag() {
         AiProxyTransport custom = request -> Mono.empty();

Reply via email to