feat: 增加服务商远端模型发现与一键添加
- 支持 OpenAI 兼容、Ollama 和阿里百炼模型目录适配 - 使用静态模型库识别能力并过滤未接入的生成模型 - 增加扁平模型列表、搜索筛选和幂等添加
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package tech.easyflow.ai.service.capability;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
|
||||
/**
|
||||
* 静态模型能力库与命名规则解析测试。
|
||||
*/
|
||||
public class ModelCapabilityResolverTest {
|
||||
|
||||
/** 待测试能力解析器。 */
|
||||
private ModelCapabilityResolver resolver;
|
||||
|
||||
/**
|
||||
* 加载真实静态模型目录。
|
||||
*/
|
||||
@Before
|
||||
public void setUp() {
|
||||
resolver = new ModelCapabilityResolver(new ModelCapabilityCatalog(new ObjectMapper()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 DashScope 模型短 ID 能命中 Alibaba 目录条目。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveCatalogCapabilitiesByProviderAlias() {
|
||||
ModelCapabilityResolution result = resolver.resolve("dashscope", "qwen3.7-plus");
|
||||
|
||||
Assert.assertEquals(ModelCapabilitySource.CATALOG, result.getSource());
|
||||
Assert.assertEquals(Model.MODEL_TYPES[0], result.getModelType());
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportImage());
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportThinking());
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportTool());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证本地补充的 BAAI 嵌入与重排模型类型互斥。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveBaaiEmbeddingAndRerankModels() {
|
||||
ModelCapabilityResolution embedding = resolver.resolve(null, "BAAI/bge-m3");
|
||||
ModelCapabilityResolution rerank = resolver.resolve(null, "bge-reranker-v2-m3");
|
||||
|
||||
Assert.assertEquals(Model.MODEL_TYPES[1], embedding.getModelType());
|
||||
Assert.assertEquals(Model.MODEL_TYPES[2], rerank.getModelType());
|
||||
Assert.assertEquals(Boolean.FALSE, embedding.getSupportTool());
|
||||
Assert.assertEquals(Boolean.FALSE, rerank.getSupportImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自定义部署名称仍能通过严格关键词识别视觉模型。
|
||||
*/
|
||||
@Test
|
||||
public void shouldInferVisionForCustomDeploymentName() {
|
||||
ModelCapabilityResolution result = resolver.resolve(
|
||||
"gpustack", "team-a/qwen2.5-vl-7b-instruct-awq");
|
||||
|
||||
Assert.assertEquals(ModelCapabilitySource.RULE, result.getSource());
|
||||
Assert.assertEquals(Model.MODEL_TYPES[0], result.getModelType());
|
||||
Assert.assertEquals(Boolean.TRUE, result.getSupportImage());
|
||||
Assert.assertNull(result.getSupportTool());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证无法识别的自定义模型保留未知能力,不误判为不支持工具。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepCapabilitiesUnknownForCustomModel() {
|
||||
ModelCapabilityResolution result = resolver.resolve("custom", "team-production-model");
|
||||
|
||||
Assert.assertEquals(ModelCapabilitySource.DEFAULT, result.getSource());
|
||||
Assert.assertEquals(Model.MODEL_TYPES[0], result.getModelType());
|
||||
Assert.assertNull(result.getSupportImage());
|
||||
Assert.assertNull(result.getSupportThinking());
|
||||
Assert.assertNull(result.getSupportTool());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tech.easyflow.ai.service.discovery;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
/**
|
||||
* 远端模型目录 URL 合并与网络目标限制测试。
|
||||
*/
|
||||
public class RemoteModelHttpClientTest {
|
||||
|
||||
/**
|
||||
* 验证 Endpoint 已含版本路径时不会重复拼接。
|
||||
*/
|
||||
@Test
|
||||
public void shouldJoinEndpointAndPathWithoutDuplicatingPrefix() {
|
||||
ModelProvider provider = provider("self-hosted", "http://127.0.0.1:8000/v1");
|
||||
|
||||
URI uri = new RemoteModelHttpClient(new ObjectMapper()).buildUri(
|
||||
provider, "/v1/models", Map.of("type", "text"));
|
||||
|
||||
Assert.assertEquals("http://127.0.0.1:8000/v1/models?type=text", uri.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证云服务商类型不能访问本机地址。
|
||||
*/
|
||||
@Test(expected = BusinessException.class)
|
||||
public void shouldRejectPrivateTargetForCloudProvider() {
|
||||
ModelProvider provider = provider("openai", "http://127.0.0.1:8000");
|
||||
|
||||
new RemoteModelHttpClient(new ObjectMapper()).buildUri(provider, "/v1/models", Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自部署类型也不能访问链路本地元数据地址。
|
||||
*/
|
||||
@Test(expected = BusinessException.class)
|
||||
public void shouldRejectMetadataTargetForSelfHostedProvider() {
|
||||
ModelProvider provider = provider("self-hosted", "http://169.254.169.254");
|
||||
|
||||
new RemoteModelHttpClient(new ObjectMapper()).buildUri(provider, "/v1/models", Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证客户端会请求并正确解压 gzip 模型目录响应。
|
||||
*
|
||||
* @throws Exception 本地测试服务启动或请求失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldRequestAndDecodeGzipResponse() throws Exception {
|
||||
AtomicReference<String> acceptEncoding = new AtomicReference<>();
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/v1/models", exchange -> {
|
||||
acceptEncoding.set(exchange.getRequestHeaders().getFirst("Accept-Encoding"));
|
||||
byte[] payload = gzip("{\"data\":[{\"id\":\"test-model\"}]}");
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.getResponseHeaders().add("Content-Encoding", "gzip");
|
||||
exchange.sendResponseHeaders(200, payload.length);
|
||||
try (var responseBody = exchange.getResponseBody()) {
|
||||
responseBody.write(payload);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
|
||||
try {
|
||||
ModelProvider provider = provider("self-hosted",
|
||||
"http://127.0.0.1:" + server.getAddress().getPort());
|
||||
|
||||
var result = new RemoteModelHttpClient(new ObjectMapper())
|
||||
.getJson(provider, "/v1/models", Map.of());
|
||||
|
||||
Assert.assertEquals("test-model", result.path("data").path(0).path("id").asText());
|
||||
Assert.assertEquals("gzip", acceptEncoding.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩测试响应内容。
|
||||
*
|
||||
* @param content 原始响应内容
|
||||
* @return gzip 压缩后的字节
|
||||
* @throws IOException 压缩失败时抛出
|
||||
*/
|
||||
private byte[] gzip(String content) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try (GZIPOutputStream gzip = new GZIPOutputStream(output)) {
|
||||
gzip.write(content.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试服务商。
|
||||
*
|
||||
* @param providerType 服务商类型
|
||||
* @param endpoint API 地址
|
||||
* @return 测试服务商
|
||||
*/
|
||||
private ModelProvider provider(String providerType, String endpoint) {
|
||||
ModelProvider provider = new ModelProvider();
|
||||
provider.setProviderType(providerType);
|
||||
provider.setEndpoint(endpoint);
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package tech.easyflow.ai.service.discovery;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.mapper.ModelMapper;
|
||||
import tech.easyflow.ai.mapper.ModelProviderMapper;
|
||||
import tech.easyflow.ai.service.ModelProviderService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityCatalog;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityResolver;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 远端模型幂等导入服务测试。
|
||||
*/
|
||||
public class RemoteModelImportServiceTest {
|
||||
|
||||
/** 测试服务商 ID。 */
|
||||
private static final BigInteger PROVIDER_ID = BigInteger.valueOf(100);
|
||||
|
||||
/** 测试服务商。 */
|
||||
private ModelProvider provider;
|
||||
/** 预置的已存在模型。 */
|
||||
private Model existingModel;
|
||||
/** 保存调用次数。 */
|
||||
private final AtomicInteger saveCount = new AtomicInteger();
|
||||
/** 待测试服务。 */
|
||||
private RemoteModelImportService importService;
|
||||
|
||||
/**
|
||||
* 初始化测试夹具。
|
||||
*/
|
||||
@Before
|
||||
public void setUp() {
|
||||
provider = new ModelProvider();
|
||||
provider.setId(PROVIDER_ID);
|
||||
provider.setProviderType("openai");
|
||||
existingModel = null;
|
||||
saveCount.set(0);
|
||||
|
||||
ModelProviderService providerService = proxy(ModelProviderService.class,
|
||||
(method, arguments) -> "getById".equals(method) ? provider : defaultValue(method));
|
||||
ModelProviderMapper providerMapper = proxy(ModelProviderMapper.class,
|
||||
(method, arguments) -> "lockById".equals(method) ? PROVIDER_ID : defaultValue(method));
|
||||
ModelMapper modelMapper = proxy(ModelMapper.class,
|
||||
(method, arguments) -> "selectOneByQuery".equals(method)
|
||||
? existingModel : defaultValue(method));
|
||||
ModelService modelService = proxy(ModelService.class, (method, arguments) -> {
|
||||
if ("save".equals(method)) {
|
||||
Model target = (Model) arguments[0];
|
||||
target.setId(BigInteger.valueOf(201));
|
||||
saveCount.incrementAndGet();
|
||||
return true;
|
||||
}
|
||||
return defaultValue(method);
|
||||
});
|
||||
ModelCapabilityCatalog catalog = new ModelCapabilityCatalog(new ObjectMapper());
|
||||
RemoteModelMetadataResolver metadataResolver = new RemoteModelMetadataResolver(
|
||||
catalog, new ModelCapabilityResolver(catalog));
|
||||
importService = new RemoteModelImportService(
|
||||
providerService, providerMapper, modelService, modelMapper, metadataResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证命中已有模型时返回幂等结果且不重复保存。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnAlreadyExistsWithoutSaving() {
|
||||
Model existing = new Model();
|
||||
existing.setId(BigInteger.valueOf(200));
|
||||
existing.setModelName("gpt-5");
|
||||
existing.setModelType(Model.MODEL_TYPES[0]);
|
||||
existingModel = existing;
|
||||
|
||||
RemoteModelImportResult result = importService.importModel(
|
||||
PROVIDER_ID, "gpt-5", new Model());
|
||||
|
||||
Assert.assertEquals(RemoteModelImportStatus.ALREADY_EXISTS, result.getStatus());
|
||||
Assert.assertEquals(existing.getId(), result.getLocalModelId());
|
||||
Assert.assertEquals(0, saveCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证新模型完成默认值补全和保存。
|
||||
*/
|
||||
@Test
|
||||
public void shouldCreateModelWhenNotExists() {
|
||||
RemoteModelImportResult result = importService.importModel(
|
||||
PROVIDER_ID, " gpt-5 ", new Model());
|
||||
|
||||
Assert.assertEquals(RemoteModelImportStatus.CREATED, result.getStatus());
|
||||
Assert.assertEquals(BigInteger.valueOf(201), result.getLocalModelId());
|
||||
Assert.assertEquals(1, saveCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建按方法名返回结果的 JDK 动态接口替身。
|
||||
*
|
||||
* @param type 接口类型
|
||||
* @param handler 方法处理器
|
||||
* @param <T> 接口类型
|
||||
* @return 接口替身
|
||||
*/
|
||||
private <T> T proxy(Class<T> type, TestInvocationHandler handler) {
|
||||
Object value = Proxy.newProxyInstance(
|
||||
type.getClassLoader(),
|
||||
new Class<?>[]{type},
|
||||
(proxy, method, arguments) -> handler.invoke(method.getName(), arguments));
|
||||
return type.cast(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回方法返回类型的基础默认值。
|
||||
*
|
||||
* @param methodName 方法名
|
||||
* @return 默认值
|
||||
*/
|
||||
private Object defaultValue(String methodName) {
|
||||
if ("count".equals(methodName)) {
|
||||
return 0L;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试接口方法处理器。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface TestInvocationHandler {
|
||||
|
||||
/**
|
||||
* 处理接口方法调用。
|
||||
*
|
||||
* @param method 方法名
|
||||
* @param arguments 方法参数
|
||||
* @return 方法结果
|
||||
*/
|
||||
Object invoke(String method, Object[] arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package tech.easyflow.ai.service.discovery;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityCatalog;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityResolver;
|
||||
|
||||
/**
|
||||
* 远端模型目录元数据和默认值补全测试。
|
||||
*/
|
||||
public class RemoteModelMetadataResolverTest {
|
||||
|
||||
/** 待测试元数据解析器。 */
|
||||
private RemoteModelMetadataResolver resolver;
|
||||
|
||||
/**
|
||||
* 加载真实静态模型目录。
|
||||
*/
|
||||
@Before
|
||||
public void setUp() {
|
||||
ModelCapabilityCatalog catalog = new ModelCapabilityCatalog(new ObjectMapper());
|
||||
resolver = new RemoteModelMetadataResolver(catalog, new ModelCapabilityResolver(catalog));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 BAAI 模型使用目录名称、家族和向量类型。
|
||||
*/
|
||||
@Test
|
||||
public void shouldEnrichBaaiEmbeddingModel() {
|
||||
RemoteModelDescriptor result = resolver.describe(null, "BAAI/bge-m3", false);
|
||||
|
||||
Assert.assertEquals("BGE-M3", result.getDisplayName());
|
||||
Assert.assertEquals("bge", result.getFamily());
|
||||
Assert.assertEquals(Model.MODEL_TYPES[1], result.getModelType());
|
||||
Assert.assertTrue(result.isAddable());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未知模型使用保守对话默认值。
|
||||
*/
|
||||
@Test
|
||||
public void shouldConfigureUnknownModelConservatively() {
|
||||
Model model = new Model();
|
||||
|
||||
resolver.configureNewModel(model, "self-hosted", "team/custom-model");
|
||||
|
||||
Assert.assertEquals("team/custom-model", model.getTitle());
|
||||
Assert.assertEquals("其他模型", model.getGroupName());
|
||||
Assert.assertEquals(Model.MODEL_TYPES[0], model.getModelType());
|
||||
Assert.assertNull(model.getSupportTool());
|
||||
Assert.assertEquals(Boolean.FALSE, model.getSupportVideo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证已知图片生成模型不会进入当前可添加范围。
|
||||
*/
|
||||
@Test
|
||||
public void shouldIdentifyUnsupportedImageGenerationModel() {
|
||||
Assert.assertTrue(resolver.isUnsupportedGenerationModel("openai", "gpt-image-1"));
|
||||
Assert.assertTrue(resolver.isUnsupportedGenerationModel("openai", "gpt-image-1.5"));
|
||||
Assert.assertFalse(resolver.isUnsupportedGenerationModel("openai", "gpt-5"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package tech.easyflow.ai.service.discovery;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 三类远端模型目录适配器测试。
|
||||
*/
|
||||
public class RemoteModelProviderAdapterTest {
|
||||
|
||||
/** JSON 解析器。 */
|
||||
private ObjectMapper objectMapper;
|
||||
/** 受控 HTTP 客户端替身。 */
|
||||
private StubRemoteModelHttpClient httpClient;
|
||||
/** 服务商配置。 */
|
||||
private ModelProvider provider;
|
||||
|
||||
/**
|
||||
* 初始化测试夹具。
|
||||
*/
|
||||
@Before
|
||||
public void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
httpClient = new StubRemoteModelHttpClient(objectMapper);
|
||||
provider = new ModelProvider();
|
||||
provider.setChatPath("/v1/chat/completions");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 OpenAI-compatible 路径推导和 data.id 解析。
|
||||
*
|
||||
* @throws Exception JSON 夹具解析失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseOpenAiCompatibleModels() throws Exception {
|
||||
provider.setProviderType("openai");
|
||||
httpClient.addResponse("{\"data\":[{\"id\":\"gpt-5\"}]}");
|
||||
|
||||
List<String> result = new OpenAiCompatibleRemoteModelAdapter()
|
||||
.fetchModelIds(provider, httpClient);
|
||||
|
||||
Assert.assertEquals(List.of("gpt-5"), result);
|
||||
Assert.assertEquals("/v1/models", httpClient.lastPath);
|
||||
Assert.assertEquals(Map.of(), httpClient.lastQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Ollama 优先读取 name 并兼容 model 字段。
|
||||
*
|
||||
* @throws Exception JSON 夹具解析失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseOllamaModels() throws Exception {
|
||||
httpClient.addResponse(
|
||||
"{\"models\":[{\"name\":\"qwen3:8b\"},{\"model\":\"bge-m3:latest\"}]}");
|
||||
|
||||
List<String> result = new OllamaRemoteModelAdapter().fetchModelIds(provider, httpClient);
|
||||
|
||||
Assert.assertEquals(List.of("qwen3:8b", "bge-m3:latest"), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证阿里百炼模型名称和分页字段解析。
|
||||
*
|
||||
* @throws Exception JSON 夹具解析失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseAliyunModels() throws Exception {
|
||||
httpClient.addResponse(
|
||||
"{\"request_id\":\"request-1\",\"output\":{"
|
||||
+ "\"page_no\":1,\"page_size\":100,\"total\":1,"
|
||||
+ "\"models\":[{\"model_name\":\"qwen-plus\"}]}}");
|
||||
|
||||
List<String> result = new AliyunRemoteModelAdapter().fetchModelIds(provider, httpClient);
|
||||
|
||||
Assert.assertEquals(List.of("qwen-plus"), result);
|
||||
Assert.assertEquals("/api/v1/deployments/models", httpClient.lastPath);
|
||||
Assert.assertEquals(Map.of(
|
||||
"model_source", "base",
|
||||
"page_no", "1",
|
||||
"page_size", "100",
|
||||
"version", "v1.0"), httpClient.lastQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证阿里百炼历史根级响应仍可解析。
|
||||
*
|
||||
* @throws Exception JSON 夹具解析失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepAliyunLegacyResponseCompatibility() throws Exception {
|
||||
httpClient.addResponse(
|
||||
"{\"models\":[{\"model_name\":\"qwen-turbo\"}],\"total_count\":1}");
|
||||
|
||||
List<String> result = new AliyunRemoteModelAdapter().fetchModelIds(provider, httpClient);
|
||||
|
||||
Assert.assertEquals(List.of("qwen-turbo"), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证百炼媒体生成和内部算法模型不会被误标为对话模型。
|
||||
*
|
||||
* @throws Exception JSON 夹具解析失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldExcludeUnsupportedAliyunModels() throws Exception {
|
||||
httpClient.addResponse(
|
||||
"{\"output\":{\"page_no\":1,\"page_size\":100,\"total\":7,"
|
||||
+ "\"models\":["
|
||||
+ "{\"model_name\":\"animate-anyone\"},"
|
||||
+ "{\"model_name\":\"animate-anyone-detect\"},"
|
||||
+ "{\"model_name\":\"emo\"},"
|
||||
+ "{\"model_name\":\"emo-detect\"},"
|
||||
+ "{\"model_name\":\"mock-algo-v1\"},"
|
||||
+ "{\"model_name\":\"wanx-v1-0521\"},"
|
||||
+ "{\"model_name\":\"qwen-plus\"}]}}"
|
||||
);
|
||||
|
||||
List<String> result = new AliyunRemoteModelAdapter().fetchModelIds(provider, httpClient);
|
||||
|
||||
Assert.assertEquals(List.of("qwen-plus"), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以队列响应替代真实网络请求的轻量测试客户端。
|
||||
*/
|
||||
private static final class StubRemoteModelHttpClient extends RemoteModelHttpClient {
|
||||
|
||||
/** JSON 解析器。 */
|
||||
private final ObjectMapper objectMapper;
|
||||
/** 待返回响应队列。 */
|
||||
private final Deque<String> responses = new ArrayDeque<>();
|
||||
/** 最近请求路径。 */
|
||||
private String lastPath;
|
||||
/** 最近查询参数。 */
|
||||
private Map<String, String> lastQuery;
|
||||
|
||||
/**
|
||||
* 创建测试客户端。
|
||||
*
|
||||
* @param objectMapper JSON 解析器
|
||||
*/
|
||||
private StubRemoteModelHttpClient(ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加下一次请求返回的 JSON。
|
||||
*
|
||||
* @param response JSON 文本
|
||||
*/
|
||||
private void addResponse(String response) {
|
||||
responses.addLast(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回预置 JSON 并记录请求参数。
|
||||
*
|
||||
* @param provider 服务商配置
|
||||
* @param requestPath 请求路径
|
||||
* @param queryParameters 查询参数
|
||||
* @return 预置 JSON 根节点
|
||||
*/
|
||||
@Override
|
||||
public com.fasterxml.jackson.databind.JsonNode getJson(
|
||||
ModelProvider provider,
|
||||
String requestPath,
|
||||
Map<String, String> queryParameters) {
|
||||
lastPath = requestPath;
|
||||
lastQuery = Map.copyOf(queryParameters);
|
||||
try {
|
||||
return objectMapper.readTree(responses.removeFirst());
|
||||
} catch (Exception exception) {
|
||||
throw new AssertionError("测试 JSON 解析失败", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user