From 66da0c9039b91d519ac80a76ef9e3f19e148e021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 17 Jul 2026 19:43:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E6=A8=A1=E5=9E=8B=20HTTP=20=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按模型地址自动选择 HTTP/1.1 或 HTTP/2 优先策略 - 复用并托管 AgentScope HTTP Transport 生命周期 - 补充协议解析、复用与 Provider 边界测试 --- .../AgentHttpTransportProvider.java | 132 ++++++++++++++++++ .../agentscope/AgentScopeModelFactory.java | 57 +++++++- .../runtime/model/AgentHttpVersionPolicy.java | 16 +++ .../agent/runtime/model/AgentModelSpec.java | 57 ++++++++ .../AgentHttpTransportProviderTest.java | 106 ++++++++++++++ 5 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProvider.java create mode 100644 easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentHttpVersionPolicy.java create mode 100644 easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProviderTest.java diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProvider.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProvider.java new file mode 100644 index 0000000..89c8884 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProvider.java @@ -0,0 +1,132 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; +import io.agentscope.core.model.transport.HttpTransport; +import io.agentscope.core.model.transport.HttpTransportConfig; +import io.agentscope.core.model.transport.HttpTransportFactory; +import io.agentscope.core.model.transport.HttpVersion; +import io.agentscope.core.model.transport.JdkHttpTransport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.http.HttpClient; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 按 HTTP 版本策略提供进程级共享的 AgentScope Transport。 + */ +public final class AgentHttpTransportProvider { + + private static final Logger LOG = LoggerFactory.getLogger(AgentHttpTransportProvider.class); + private static final AgentHttpTransportProvider SHARED = new AgentHttpTransportProvider(); + + private final Map transports = new ConcurrentHashMap<>(); + + /** + * 创建 Transport 提供器。 + */ + private AgentHttpTransportProvider() { + } + + /** + * 获取进程级共享提供器。 + * + * @return 共享提供器 + */ + public static AgentHttpTransportProvider shared() { + return SHARED; + } + + /** + * 获取指定策略与基础 URL 对应的共享 Transport。 + * + * @param policy HTTP 版本策略 + * @param baseUrl 最终生效的模型基础 URL + * @return 共享 Transport + */ + public HttpTransport getTransport(AgentHttpVersionPolicy policy, String baseUrl) { + AgentHttpVersionPolicy effectivePolicy = resolveEffectivePolicy(policy, baseUrl); + if (effectivePolicy == AgentHttpVersionPolicy.AUTO) { + return HttpTransportFactory.getDefault(); + } + return transports.computeIfAbsent(effectivePolicy, this::createTransport); + } + + /** + * 解析请求实际使用的 HTTP 策略。 + * + *

明文 HTTP 固定使用 HTTP/1.1,避免 JDK 客户端发起 h2c Upgrade;HTTPS + * 保持 HTTP/2 优先并允许底层通过 ALPN 回退。显式策略始终覆盖 URL 判断。

+ * + * @param policy 配置的 HTTP 版本策略 + * @param baseUrl 最终生效的模型基础 URL + * @return 实际生效策略;无法识别 URL 时返回 AUTO + */ + public static AgentHttpVersionPolicy resolveEffectivePolicy(AgentHttpVersionPolicy policy, String baseUrl) { + AgentHttpVersionPolicy safePolicy = policy == null ? AgentHttpVersionPolicy.AUTO : policy; + if (safePolicy != AgentHttpVersionPolicy.AUTO) { + return safePolicy; + } + if (baseUrl == null || baseUrl.isBlank()) { + LOG.warn("Agent HTTP AUTO policy cannot infer protocol because base URL is missing; fallback to default transport"); + return AgentHttpVersionPolicy.AUTO; + } + try { + String scheme = URI.create(baseUrl.trim()).getScheme(); + if (scheme == null) { + return AgentHttpVersionPolicy.AUTO; + } + String normalizedScheme = scheme.toLowerCase(Locale.ROOT); + if ("http".equals(normalizedScheme)) { + return AgentHttpVersionPolicy.HTTP_1_1; + } + if ("https".equals(normalizedScheme)) { + return AgentHttpVersionPolicy.HTTP_2_PREFERRED; + } + LOG.warn("Agent HTTP AUTO policy does not support URL scheme '{}'; fallback to default transport", + normalizedScheme); + return AgentHttpVersionPolicy.AUTO; + } catch (IllegalArgumentException exception) { + LOG.warn("Agent HTTP AUTO policy cannot parse base URL; fallback to default transport"); + return AgentHttpVersionPolicy.AUTO; + } + } + + /** + * 创建并注册受 AgentScope 生命周期管理的 JDK Transport。 + * + * @param policy HTTP 版本策略 + * @return 新 Transport + */ + private HttpTransport createTransport(AgentHttpVersionPolicy policy) { + HttpVersion httpVersion = resolveHttpVersion(policy); + HttpTransportConfig config = HttpTransportConfig.builder() + .httpVersion(httpVersion) + .build(); + HttpClient httpClient = HttpClient.newBuilder() + .version(httpVersion.toJdkHttpVersion()) + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(config.getConnectTimeout()) + .build(); + HttpTransport transport = new JdkHttpTransport(httpClient, config); + // 注册后由 AgentScope JVM shutdown hook 统一关闭,避免每次 Agent 运行创建连接池。 + HttpTransportFactory.register(transport); + return transport; + } + + /** + * 将中立策略映射为 AgentScope HTTP 版本。 + * + * @param policy HTTP 版本策略 + * @return AgentScope HTTP 版本 + */ + static HttpVersion resolveHttpVersion(AgentHttpVersionPolicy policy) { + if (policy == AgentHttpVersionPolicy.HTTP_1_1) { + return HttpVersion.HTTP_1_1; + } + return HttpVersion.HTTP_2; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java index d408b31..15437d0 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java @@ -2,6 +2,7 @@ package com.easyagents.agent.runtime.agentscope; import com.easyagents.agent.runtime.AgentRuntimeException; import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; import com.easyagents.agent.runtime.model.AgentModelFactory; import com.easyagents.agent.runtime.model.AgentModelProviderType; import com.easyagents.agent.runtime.model.AgentModelSpec; @@ -24,6 +25,24 @@ public class AgentScopeModelFactory implements AgentModelFactory { private static final String ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"; private static final String SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"; + private final AgentHttpTransportProvider httpTransportProvider; + + /** + * 使用进程级共享 Transport 提供器创建模型工厂。 + */ + public AgentScopeModelFactory() { + this(AgentHttpTransportProvider.shared()); + } + + /** + * 使用指定 Transport 提供器创建模型工厂。 + * + * @param httpTransportProvider Transport 提供器 + */ + AgentScopeModelFactory(AgentHttpTransportProvider httpTransportProvider) { + this.httpTransportProvider = httpTransportProvider; + } + @Override public Model create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions) { if (modelSpec == null) { @@ -31,6 +50,7 @@ public class AgentScopeModelFactory implements AgentModelFactory { } GenerateOptions options = toGenerateOptions(modelSpec, generationOptions); AgentModelProviderType providerType = modelSpec.getProviderType(); + validateHttpTransportSupport(providerType, modelSpec.getHttpVersionPolicy()); if (providerType == AgentModelProviderType.OLLAMA) { return buildOllama(modelSpec, options); } @@ -124,12 +144,14 @@ public class AgentScopeModelFactory implements AgentModelFactory { * @return 模型 */ private Model buildOpenAiCompatible(AgentModelSpec modelSpec, GenerateOptions options, String defaultBaseUrl) { + String baseUrl = resolveBaseUrl(modelSpec, defaultBaseUrl); OpenAIChatModel.Builder builder = OpenAIChatModel.builder() .apiKey(modelSpec.getApiKey()) .modelName(modelSpec.getModelName()) - .baseUrl(resolveBaseUrl(modelSpec, defaultBaseUrl)) + .baseUrl(baseUrl) .endpointPath(modelSpec.getEndpointPath()) .stream(Boolean.TRUE.equals(options.getStream())) + .httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl)) .generateOptions(options); return builder.build(); } @@ -176,12 +198,14 @@ public class AgentScopeModelFactory implements AgentModelFactory { * @return 模型 */ private Model buildDeepSeek(AgentModelSpec modelSpec, GenerateOptions options) { + String baseUrl = resolveBaseUrl(modelSpec, DEEPSEEK_BASE_URL); OpenAIChatModel.Builder builder = OpenAIChatModel.builder() .apiKey(modelSpec.getApiKey()) .modelName(modelSpec.getModelName()) - .baseUrl(resolveBaseUrl(modelSpec, DEEPSEEK_BASE_URL)) + .baseUrl(baseUrl) .endpointPath(modelSpec.getEndpointPath()) .stream(Boolean.TRUE.equals(options.getStream())) + .httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl)) .formatter(new DeepSeekFormatter()) .generateOptions(options); return builder.build(); @@ -195,12 +219,14 @@ public class AgentScopeModelFactory implements AgentModelFactory { * @return 模型 */ private Model buildGlm(AgentModelSpec modelSpec, GenerateOptions options) { + String baseUrl = resolveBaseUrl(modelSpec, GLM_BASE_URL); OpenAIChatModel.Builder builder = OpenAIChatModel.builder() .apiKey(modelSpec.getApiKey()) .modelName(modelSpec.getModelName()) - .baseUrl(resolveBaseUrl(modelSpec, GLM_BASE_URL)) + .baseUrl(baseUrl) .endpointPath(modelSpec.getEndpointPath()) .stream(Boolean.TRUE.equals(options.getStream())) + .httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl)) .formatter(new GLMFormatter()) .generateOptions(options); return builder.build(); @@ -231,16 +257,39 @@ public class AgentScopeModelFactory implements AgentModelFactory { */ private Model buildDashScope(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions, GenerateOptions options) { Boolean thinkingEnabled = generationOptions == null ? null : generationOptions.getThinkingEnabled(); + String baseUrl = modelSpec.getBaseUrl(); return DashScopeChatModel.builder() .apiKey(modelSpec.getApiKey()) .modelName(modelSpec.getModelName()) - .baseUrl(modelSpec.getBaseUrl()) + .baseUrl(baseUrl) .stream(Boolean.TRUE.equals(options.getStream())) .enableThinking(thinkingEnabled) + .httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl)) .defaultOptions(options) .build(); } + /** + * 校验当前 Provider 是否支持显式 Agent HTTP Transport。 + * + * @param providerType 模型供应商类型 + * @param policy HTTP 版本策略 + */ + private void validateHttpTransportSupport(AgentModelProviderType providerType, + AgentHttpVersionPolicy policy) { + if (policy == null || policy == AgentHttpVersionPolicy.AUTO) { + return; + } + if (providerType == AgentModelProviderType.ANTHROPIC + || providerType == AgentModelProviderType.GEMINI + || providerType == AgentModelProviderType.OLLAMA) { + throw new AgentRuntimeException( + "Agent HTTP transport policy " + policy + + " is not supported by provider " + providerType + + "; use AUTO for this provider."); + } + } + /** * 优先使用调用方传入的基础 URL,未传入时使用供应商默认地址。 * diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentHttpVersionPolicy.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentHttpVersionPolicy.java new file mode 100644 index 0000000..583aca6 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentHttpVersionPolicy.java @@ -0,0 +1,16 @@ +package com.easyagents.agent.runtime.model; + +/** + * Agent 模型调用使用的 HTTP 版本策略。 + */ +public enum AgentHttpVersionPolicy { + + /** 按基础 URL 协议自动选择:HTTP 使用 1.1,HTTPS 优先使用 2。 */ + AUTO, + + /** 强制使用 HTTP/1.1。 */ + HTTP_1_1, + + /** 优先使用 HTTP/2,并允许 JDK 客户端按协议能力回退。 */ + HTTP_2_PREFERRED +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java index e61ee99..59bc84a 100644 --- a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java @@ -13,6 +13,9 @@ public class AgentModelSpec { private String baseUrl; private String endpointPath; private String apiKey; + private boolean supportImage; + private boolean supportImageBase64Only; + private AgentHttpVersionPolicy httpVersionPolicy = AgentHttpVersionPolicy.AUTO; private Map metadata = new LinkedHashMap<>(); /** @@ -105,6 +108,60 @@ public class AgentModelSpec { this.apiKey = apiKey; } + /** + * 判断模型是否支持图片输入。 + * + * @return 支持图片时返回 true + */ + public boolean isSupportImage() { + return supportImage; + } + + /** + * 设置模型是否支持图片输入。 + * + * @param supportImage 是否支持图片 + */ + public void setSupportImage(boolean supportImage) { + this.supportImage = supportImage; + } + + /** + * 判断模型是否只接受 Base64 图片。 + * + * @return 仅接受 Base64 时返回 true + */ + public boolean isSupportImageBase64Only() { + return supportImageBase64Only; + } + + /** + * 设置模型是否只接受 Base64 图片。 + * + * @param supportImageBase64Only 是否只接受 Base64 图片 + */ + public void setSupportImageBase64Only(boolean supportImageBase64Only) { + this.supportImageBase64Only = supportImageBase64Only; + } + + /** + * 获取 Agent 模型调用的 HTTP 版本策略。 + * + * @return HTTP 版本策略 + */ + public AgentHttpVersionPolicy getHttpVersionPolicy() { + return httpVersionPolicy; + } + + /** + * 设置 Agent 模型调用的 HTTP 版本策略。 + * + * @param httpVersionPolicy HTTP 版本策略 + */ + public void setHttpVersionPolicy(AgentHttpVersionPolicy httpVersionPolicy) { + this.httpVersionPolicy = httpVersionPolicy == null ? AgentHttpVersionPolicy.AUTO : httpVersionPolicy; + } + /** * 获取元数据。 * diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProviderTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProviderTest.java new file mode 100644 index 0000000..a980ea7 --- /dev/null +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentHttpTransportProviderTest.java @@ -0,0 +1,106 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.AgentRuntimeException; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.model.transport.HttpTransport; +import io.agentscope.core.model.transport.HttpTransportFactory; +import io.agentscope.core.model.transport.HttpVersion; +import io.agentscope.core.model.transport.JdkHttpTransport; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.net.http.HttpClient; + +/** + * Agent HTTP Transport 策略测试。 + */ +public class AgentHttpTransportProviderTest { + + /** + * 验证相同策略复用 Transport,且显式策略使用预期 JDK HTTP 版本。 + * + * @throws Exception 反射读取 JDK client 失败时抛出 + */ + @Test + public void shouldReuseTransportAndConfigureExpectedHttpVersion() throws Exception { + AgentHttpTransportProvider provider = AgentHttpTransportProvider.shared(); + + HttpTransport http11 = provider.getTransport(AgentHttpVersionPolicy.HTTP_1_1, "https://example.com"); + HttpTransport http11Again = provider.getTransport(AgentHttpVersionPolicy.HTTP_1_1, "http://example.com"); + HttpTransport http2 = provider.getTransport(AgentHttpVersionPolicy.HTTP_2_PREFERRED, "http://example.com"); + + Assert.assertSame(http11, http11Again); + Assert.assertNotSame(http11, http2); + Assert.assertEquals(HttpClient.Version.HTTP_1_1, httpClient(http11).version()); + Assert.assertEquals(HttpClient.Version.HTTP_2, httpClient(http2).version()); + Assert.assertTrue(HttpTransportFactory.isManaged(http11)); + Assert.assertTrue(HttpTransportFactory.isManaged(http2)); + Assert.assertEquals(HttpVersion.HTTP_1_1, + AgentHttpTransportProvider.resolveHttpVersion(AgentHttpVersionPolicy.HTTP_1_1)); + Assert.assertEquals(HttpVersion.HTTP_2, + AgentHttpTransportProvider.resolveHttpVersion(AgentHttpVersionPolicy.HTTP_2_PREFERRED)); + } + + /** + * 验证 AUTO 根据 URL 协议选择安全 Transport。 + * + * @throws Exception 反射读取 JDK client 失败时抛出 + */ + @Test + public void autoShouldResolveTransportFromBaseUrlScheme() throws Exception { + AgentHttpTransportProvider provider = AgentHttpTransportProvider.shared(); + HttpTransport http = provider.getTransport(AgentHttpVersionPolicy.AUTO, "http://example.com/v1"); + HttpTransport https = provider.getTransport(AgentHttpVersionPolicy.AUTO, "https://example.com/v1"); + HttpTransport unknown = provider.getTransport(AgentHttpVersionPolicy.AUTO, "example.com/v1"); + + Assert.assertSame(provider.getTransport(AgentHttpVersionPolicy.HTTP_1_1, null), http); + Assert.assertSame(provider.getTransport(AgentHttpVersionPolicy.HTTP_2_PREFERRED, null), https); + Assert.assertEquals(HttpClient.Version.HTTP_1_1, httpClient(http).version()); + Assert.assertEquals(HttpClient.Version.HTTP_2, httpClient(https).version()); + Assert.assertSame(HttpTransportFactory.getDefault(), unknown); + Assert.assertEquals(AgentHttpVersionPolicy.HTTP_1_1, + AgentHttpTransportProvider.resolveEffectivePolicy(AgentHttpVersionPolicy.AUTO, "HTTP://EXAMPLE.COM")); + Assert.assertEquals(AgentHttpVersionPolicy.HTTP_2_PREFERRED, + AgentHttpTransportProvider.resolveEffectivePolicy(AgentHttpVersionPolicy.AUTO, "HTTPS://EXAMPLE.COM")); + Assert.assertEquals(AgentHttpVersionPolicy.AUTO, + AgentHttpTransportProvider.resolveEffectivePolicy(AgentHttpVersionPolicy.AUTO, "not a uri")); + } + + /** + * 验证不支持注入 Transport 的 Provider 会返回明确错误。 + */ + @Test + public void unsupportedProviderShouldRejectExplicitHttpPolicy() { + AgentModelSpec spec = new AgentModelSpec(); + spec.setProviderType(AgentModelProviderType.ANTHROPIC); + spec.setModelName("claude-test"); + spec.setApiKey("test-key"); + spec.setHttpVersionPolicy(AgentHttpVersionPolicy.HTTP_1_1); + + try { + new AgentScopeModelFactory().create(spec, new AgentGenerationOptions()); + Assert.fail("Expected unsupported HTTP transport policy error"); + } catch (AgentRuntimeException exception) { + Assert.assertTrue(exception.getMessage().contains("HTTP_1_1")); + Assert.assertTrue(exception.getMessage().contains("ANTHROPIC")); + } + } + + /** + * 获取 Transport 内部复用的 JDK HttpClient。 + * + * @param transport Transport 实例 + * @return JDK HttpClient + * @throws Exception 反射失败时抛出 + */ + private HttpClient httpClient(HttpTransport transport) throws Exception { + Assert.assertTrue(transport instanceof JdkHttpTransport); + Field field = JdkHttpTransport.class.getDeclaredField("client"); + field.setAccessible(true); + return (HttpClient) field.get(transport); + } +}