feat: 增加智能体模型 HTTP 传输兼容策略

- 按模型地址自动选择 HTTP/1.1 或 HTTP/2 优先策略

- 复用并托管 AgentScope HTTP Transport 生命周期

- 补充协议解析、复用与 Provider 边界测试
This commit is contained in:
2026-07-17 19:43:55 +08:00
parent 9d0d148415
commit 66da0c9039
5 changed files with 364 additions and 4 deletions

View File

@@ -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<AgentHttpVersionPolicy, HttpTransport> 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 策略。
*
* <p>明文 HTTP 固定使用 HTTP/1.1,避免 JDK 客户端发起 h2c UpgradeHTTPS
* 保持 HTTP/2 优先并允许底层通过 ALPN 回退。显式策略始终覆盖 URL 判断。</p>
*
* @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;
}
}

View File

@@ -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<Model> {
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<Model> {
}
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<Model> {
* @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<Model> {
* @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<Model> {
* @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<Model> {
*/
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未传入时使用供应商默认地址。
*

View File

@@ -0,0 +1,16 @@
package com.easyagents.agent.runtime.model;
/**
* Agent 模型调用使用的 HTTP 版本策略。
*/
public enum AgentHttpVersionPolicy {
/** 按基础 URL 协议自动选择HTTP 使用 1.1HTTPS 优先使用 2。 */
AUTO,
/** 强制使用 HTTP/1.1。 */
HTTP_1_1,
/** 优先使用 HTTP/2并允许 JDK 客户端按协议能力回退。 */
HTTP_2_PREFERRED
}

View File

@@ -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<String, Object> 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;
}
/**
* 获取元数据。
*

View File

@@ -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);
}
}