发布 v1.1.0 #2
@@ -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 Upgrade;HTTPS
|
||||
* 保持 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;
|
||||
}
|
||||
}
|
||||
@@ -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,未传入时使用供应商默认地址。
|
||||
*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user