From aaf4c61ff8fb75f7cd218ea8675ee215bd907837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Thu, 26 Mar 2026 20:48:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=BD=91=E5=85=B3=E4=B8=8E=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=B7=A5=E4=BD=9C=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 OpenAI 兼容统一模型调用链路、模型发布配置与批量发布能力 - 重构模型管理页面入口与统一网关工作区,更新服务商 logo 资源与模型 ID 文案 - 收口全新库初始化脚本,仅保留服务商种子并整理统一网关 migration --- .../admin/controller/ai/ModelController.java | 32 + .../controller/PublicModelChatController.java | 176 +++ easyflow-modules/easyflow-module-ai/pom.xml | 4 + .../ai/dto/ModelInvokeConfigDtos.java | 65 + .../easyflow/ai/entity/base/ModelBase.java | 28 + .../exception/ModelInvokeException.java | 61 + .../invoke/mapper/OpenAiProtocolMapper.java | 651 +++++++++ .../ai/invoke/model/UnifiedChatChunk.java | 61 + .../ai/invoke/model/UnifiedChatRequest.java | 99 ++ .../ai/invoke/model/UnifiedChatResponse.java | 61 + .../ai/invoke/model/UnifiedChoice.java | 41 + .../ai/invoke/model/UnifiedContentPart.java | 32 + .../ai/invoke/model/UnifiedImageUrl.java | 23 + .../ai/invoke/model/UnifiedMessage.java | 61 + .../invoke/model/UnifiedResponseFormat.java | 25 + .../easyflow/ai/invoke/model/UnifiedTool.java | 23 + .../ai/invoke/model/UnifiedToolCall.java | 41 + .../invoke/model/UnifiedToolCallFunction.java | 23 + .../ai/invoke/model/UnifiedToolFunction.java | 34 + .../ai/invoke/model/UnifiedUsage.java | 32 + .../OpenAiChatCompletionChunkResponse.java | 134 ++ .../openai/OpenAiChatCompletionRequest.java | 335 +++++ .../openai/OpenAiChatCompletionResponse.java | 247 ++++ .../protocol/openai/OpenAiErrorResponse.java | 54 + .../protocol/openai/OpenAiJsonSupport.java | 20 + .../invoke/provider/ModelProviderGateway.java | 14 + .../ModelProviderGatewayRegistry.java | 27 + .../provider/UnifiedChatChunkObserver.java | 14 + .../base/AbstractOpenAiCompatibleGateway.java | 129 ++ .../invoke/provider/openai/OpenAiGateway.java | 78 + .../service/UnifiedModelInvokeService.java | 12 + .../impl/UnifiedModelInvokeServiceImpl.java | 114 ++ .../easyflow/ai/service/ModelService.java | 10 + .../ai/service/impl/ModelServiceImpl.java | 120 ++ .../mapper/OpenAiProtocolMapperTest.java | 192 +++ .../service/impl/SysApiKeyServiceImpl.java | 24 +- .../db/migration/V3__easyflow_seed.sql | 28 - .../db/migration/V7__model_invoke_gateway.sql | 18 + .../app/public/model-providers/aliyun.png | Bin 7399 -> 0 bytes .../app/public/model-providers/deepseek.png | Bin 8803 -> 0 bytes .../app/public/model-providers/kimi.png | Bin 7295 -> 0 bytes .../model-providers/lobehub/bailian.svg | 1 + .../public/model-providers/lobehub/claude.svg | 1 + .../model-providers/lobehub/deepseek.svg | 1 + .../public/model-providers/lobehub/doubao.svg | 2 + .../public/model-providers/lobehub/gemini.svg | 2 + .../public/model-providers/lobehub/kimi.svg | 1 + .../model-providers/lobehub/minimax.svg | 1 + .../public/model-providers/lobehub/ollama.svg | 1 + .../public/model-providers/lobehub/openai.svg | 1 + .../public/model-providers/lobehub/qwen.svg | 1 + .../model-providers/lobehub/siliconcloud.svg | 1 + .../public/model-providers/lobehub/vllm.svg | 1 + .../public/model-providers/lobehub/zhipu.svg | 1 + .../app/public/model-providers/minimax.png | Bin 1877 -> 0 bytes .../app/public/model-providers/ollama.png | Bin 1797 -> 0 bytes .../app/public/model-providers/openai.ico | 1 - .../app/public/model-providers/openai.svg | 10 - .../public/model-providers/self-hosted.svg | 8 - .../public/model-providers/siliconflow.png | Bin 583 -> 0 bytes .../app/public/model-providers/zhipu.png | Bin 2747 -> 0 bytes easyflow-ui-admin/app/src/api/ai/llm.ts | 35 +- .../app/src/locales/langs/en-US/llm.json | 66 +- .../locales/langs/en-US/settingsConfig.json | 2 +- .../app/src/locales/langs/zh-CN/llm.json | 17 +- .../locales/langs/zh-CN/settingsConfig.json | 2 +- .../views/ai/model/ActiveModelWorkspace.vue | 25 +- .../app/src/views/ai/model/AddModelModal.vue | 331 +++-- .../app/src/views/ai/model/Model.vue | 59 +- .../src/views/ai/model/ModelProviderBadge.vue | 4 +- .../views/ai/model/ModelViewItemOperation.vue | 36 +- .../ai/model/UnifiedGatewayWorkspace.vue | 1260 +++++++++++++++++ .../modelUtils/__tests__/defaultIcon.test.ts | 16 +- .../ai/model/modelUtils/model-ability.ts | 11 - .../ai/model/modelUtils/providerList.json | 18 +- .../app/src/locales/langs/en-US/llm.json | 64 +- .../locales/langs/en-US/settingsConfig.json | 2 +- .../app/src/locales/langs/zh-CN/llm.json | 14 +- .../locales/langs/zh-CN/settingsConfig.json | 4 +- pom.xml | 5 + 80 files changed, 4786 insertions(+), 362 deletions(-) create mode 100644 easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicModelChatController.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/ModelInvokeConfigDtos.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/exception/ModelInvokeException.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapper.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatChunk.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatRequest.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatResponse.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChoice.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedContentPart.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedImageUrl.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedMessage.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedResponseFormat.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedTool.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCall.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCallFunction.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolFunction.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedUsage.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionChunkResponse.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionRequest.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionResponse.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiErrorResponse.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiJsonSupport.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGateway.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGatewayRegistry.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/UnifiedChatChunkObserver.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/base/AbstractOpenAiCompatibleGateway.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/openai/OpenAiGateway.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/UnifiedModelInvokeService.java create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/impl/UnifiedModelInvokeServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapperTest.java create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V7__model_invoke_gateway.sql delete mode 100644 easyflow-ui-admin/app/public/model-providers/aliyun.png delete mode 100644 easyflow-ui-admin/app/public/model-providers/deepseek.png delete mode 100644 easyflow-ui-admin/app/public/model-providers/kimi.png create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/bailian.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/claude.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/deepseek.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/doubao.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/gemini.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/kimi.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/minimax.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/ollama.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/openai.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/qwen.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/siliconcloud.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/vllm.svg create mode 100644 easyflow-ui-admin/app/public/model-providers/lobehub/zhipu.svg delete mode 100644 easyflow-ui-admin/app/public/model-providers/minimax.png delete mode 100644 easyflow-ui-admin/app/public/model-providers/ollama.png delete mode 100644 easyflow-ui-admin/app/public/model-providers/openai.ico delete mode 100644 easyflow-ui-admin/app/public/model-providers/openai.svg delete mode 100644 easyflow-ui-admin/app/public/model-providers/self-hosted.svg delete mode 100644 easyflow-ui-admin/app/public/model-providers/siliconflow.png delete mode 100644 easyflow-ui-admin/app/public/model-providers/zhipu.png create mode 100644 easyflow-ui-admin/app/src/views/ai/model/UnifiedGatewayWorkspace.vue diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java index 6f248f3..d9728ac 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/ModelController.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; +import tech.easyflow.ai.dto.ModelInvokeConfigDtos; import tech.easyflow.ai.entity.Model; import tech.easyflow.ai.entity.ModelProvider; import tech.easyflow.ai.entity.table.ModelTableDef; @@ -69,6 +70,12 @@ public class ModelController extends BaseCurdController { return Result.ok(modelService.getList(entity)); } + @GetMapping("invokeList") + @SaCheckPermission("/api/v1/model/query") + public Result> invokeList() { + return Result.ok(modelService.listInvokeModels()); + } + @PostMapping("/addAiLlm") @SaCheckPermission("/api/v1/model/save") public Result addAiLlm(Model entity) { @@ -92,6 +99,31 @@ public class ModelController extends BaseCurdController { return Result.ok(); } + @PostMapping("/updateInvokeConfig") + @SaCheckPermission("/api/v1/model/save") + public Result updateInvokeConfig(@RequestBody ModelInvokeConfigDtos.UpdateRequest request) { + return Result.ok(modelService.updateInvokeConfig( + request.getId(), + request.getInvokeCode(), + request.getPublishEnabled() + )); + } + + @PostMapping("/batchUpdateInvokePublishStatus") + @SaCheckPermission("/api/v1/model/save") + public Result> batchUpdateInvokePublishStatus(@RequestBody ModelInvokeConfigDtos.BatchPublishRequest request) { + return Result.ok(modelService.batchUpdateInvokePublishStatus( + request.getIds(), + request.getPublishEnabled() + )); + } + + @Override + protected Result onSaveOrUpdateBefore(Model entity, boolean isSave) { + modelService.validateForSaveOrUpdate(entity, isSave); + return super.onSaveOrUpdateBefore(entity, isSave); + } + @GetMapping("/selectLlmByProviderCategory") @SaCheckPermission("/api/v1/model/query") public Result>> selectLlmByProviderCategory(Model entity, String sortKey, String sortType) { diff --git a/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicModelChatController.java b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicModelChatController.java new file mode 100644 index 0000000..26da52e --- /dev/null +++ b/easyflow-api/easyflow-api-public/src/main/java/tech/easyflow/publicapi/controller/PublicModelChatController.java @@ -0,0 +1,176 @@ +package tech.easyflow.publicapi.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import tech.easyflow.ai.invoke.exception.ModelInvokeException; +import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper; +import tech.easyflow.ai.invoke.model.UnifiedChatChunk; +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiErrorResponse; +import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver; +import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.system.service.SysApiKeyService; + +import javax.annotation.Resource; +import java.io.IOException; +import java.time.Duration; + +@SaIgnore +@RestController +@RequestMapping("/v1") +public class PublicModelChatController { + + private static final Logger log = LoggerFactory.getLogger(PublicModelChatController.class); + private static final long SSE_TIMEOUT = Duration.ofMinutes(10).toMillis(); + + @Resource + private SysApiKeyService sysApiKeyService; + @Resource + private UnifiedModelInvokeService unifiedModelInvokeService; + @Resource + private OpenAiProtocolMapper openAiProtocolMapper; + @Resource + private ObjectMapper objectMapper; + + /** + * 统一模型调用(OpenAI Chat Completions) + */ + @PostMapping( + value = "/chat/completions", + produces = { + MediaType.APPLICATION_JSON_VALUE, + MediaType.TEXT_EVENT_STREAM_VALUE + } + ) + public Object chatCompletions(@RequestBody String rawBody, HttpServletRequest request) { + try { + String apiKey = resolveApiKey(request.getHeader(HttpHeaders.AUTHORIZATION)); + sysApiKeyService.checkApikeyPermission(apiKey, request.getRequestURI()); + + OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.readRequest(rawBody); + UnifiedChatRequest unifiedRequest = openAiProtocolMapper.toUnifiedRequest(openAiRequest); + if (Boolean.TRUE.equals(unifiedRequest.getStream())) { + return createStreamEmitter(unifiedRequest); + } + UnifiedChatResponse response = unifiedModelInvokeService.chat(unifiedRequest); + return buildJsonResponse(openAiProtocolMapper.toOpenAiResponse(response)); + } catch (ModelInvokeException e) { + return buildErrorResponse(e); + } catch (BusinessException e) { + return buildErrorResponse(mapBusinessException(e)); + } catch (Exception e) { + log.error("chatCompletions unexpected error: {}", e.getMessage(), e); + return buildErrorResponse(new ModelInvokeException( + 500, + "统一模型调用失败: " + e.getMessage(), + "api_error", + null, + "internal_error" + )); + } + } + + private SseEmitter createStreamEmitter(UnifiedChatRequest unifiedRequest) { + SseEmitter emitter = new SseEmitter(SSE_TIMEOUT); + unifiedModelInvokeService.chatStream(unifiedRequest, new UnifiedChatChunkObserver() { + @Override + public void onChunk(UnifiedChatChunk chunk) { + try { + String payload = objectMapper.writeValueAsString(openAiProtocolMapper.toOpenAiChunk(chunk)); + emitter.send(SseEmitter.event().data(payload)); + } catch (IOException e) { + emitter.completeWithError(e); + } + } + + @Override + public void onComplete() { + try { + emitter.send(SseEmitter.event().data("[DONE]")); + emitter.complete(); + } catch (IOException e) { + emitter.completeWithError(e); + } + } + + @Override + public void onError(Throwable throwable) { + log.error("chatCompletions stream error: {}", throwable.getMessage(), throwable); + emitter.completeWithError(throwable); + } + }); + return emitter; + } + + private ResponseEntity buildJsonResponse(Object body) { + try { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(body)); + } catch (Exception e) { + throw new RuntimeException("序列化响应失败", e); + } + } + + private ResponseEntity buildErrorResponse(ModelInvokeException e) { + OpenAiErrorResponse response = new OpenAiErrorResponse(); + OpenAiErrorResponse.Error error = new OpenAiErrorResponse.Error(); + error.setMessage(e.getMessage()); + error.setType(e.getType()); + error.setParam(e.getParam()); + error.setCode(e.getCode()); + response.setError(error); + return ResponseEntity.status(e.getStatus()) + .contentType(MediaType.APPLICATION_JSON) + .body(writeJson(response)); + } + + private String writeJson(Object body) { + try { + return objectMapper.writeValueAsString(body); + } catch (Exception ex) { + log.error("writeJson error: {}", ex.getMessage(), ex); + return "{\"error\":{\"message\":\"响应序列化失败\",\"type\":\"api_error\",\"code\":\"serialization_error\"}}"; + } + } + + private ModelInvokeException mapBusinessException(BusinessException e) { + String message = StrUtil.blankToDefault(e.getMessage(), "访问令牌校验失败"); + if (StrUtil.containsAnyIgnoreCase(message, "apikey 不存在", "apikey 已过期", "已禁用")) { + return ModelInvokeException.unauthorized(message); + } + if (StrUtil.containsAnyIgnoreCase(message, "无权限", "接口不存在")) { + return ModelInvokeException.forbidden(message); + } + return ModelInvokeException.badRequest(message); + } + + private String resolveApiKey(String authorizationHeader) { + if (StrUtil.isBlank(authorizationHeader)) { + throw ModelInvokeException.unauthorized("Authorization 不能为空"); + } + String trimmed = authorizationHeader.trim(); + if (StrUtil.startWithIgnoreCase(trimmed, "Bearer ")) { + trimmed = trimmed.substring(7).trim(); + } + if (StrUtil.isBlank(trimmed)) { + throw ModelInvokeException.unauthorized("Authorization 无效"); + } + return trimmed; + } +} diff --git a/easyflow-modules/easyflow-module-ai/pom.xml b/easyflow-modules/easyflow-module-ai/pom.xml index f714ace..a14c3b8 100644 --- a/easyflow-modules/easyflow-module-ai/pom.xml +++ b/easyflow-modules/easyflow-module-ai/pom.xml @@ -41,6 +41,10 @@ com.easyagents easy-agents-support + + com.easyagents + easy-agents-spring-boot-starter + diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/ModelInvokeConfigDtos.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/ModelInvokeConfigDtos.java new file mode 100644 index 0000000..52fd441 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/ModelInvokeConfigDtos.java @@ -0,0 +1,65 @@ +package tech.easyflow.ai.dto; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.List; + +public class ModelInvokeConfigDtos { + + public static class UpdateRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + private BigInteger id; + private String invokeCode; + private Boolean publishEnabled; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getInvokeCode() { + return invokeCode; + } + + public void setInvokeCode(String invokeCode) { + this.invokeCode = invokeCode; + } + + public Boolean getPublishEnabled() { + return publishEnabled; + } + + public void setPublishEnabled(Boolean publishEnabled) { + this.publishEnabled = publishEnabled; + } + } + + public static class BatchPublishRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + private List ids; + private Boolean publishEnabled; + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + public Boolean getPublishEnabled() { + return publishEnabled; + } + + public void setPublishEnabled(Boolean publishEnabled) { + this.publishEnabled = publishEnabled; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/ModelBase.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/ModelBase.java index a71e627..befc124 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/ModelBase.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/base/ModelBase.java @@ -152,6 +152,18 @@ public class ModelBase implements Serializable { @Column(comment = "是否支持tool消息") private Boolean supportToolMessage; + /** + * 统一模型调用对外标识 + */ + @Column(comment = "统一模型调用对外标识") + private String invokeCode; + + /** + * 是否开启统一模型调用发布 + */ + @Column(comment = "是否开启统一模型调用发布") + private Boolean publishEnabled; + public BigInteger getId() { return id; } @@ -336,4 +348,20 @@ public class ModelBase implements Serializable { this.supportToolMessage = supportToolMessage; } + public String getInvokeCode() { + return invokeCode; + } + + public void setInvokeCode(String invokeCode) { + this.invokeCode = invokeCode; + } + + public Boolean getPublishEnabled() { + return publishEnabled; + } + + public void setPublishEnabled(Boolean publishEnabled) { + this.publishEnabled = publishEnabled; + } + } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/exception/ModelInvokeException.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/exception/ModelInvokeException.java new file mode 100644 index 0000000..e2faed7 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/exception/ModelInvokeException.java @@ -0,0 +1,61 @@ +package tech.easyflow.ai.invoke.exception; + +public class ModelInvokeException extends RuntimeException { + + private final int status; + private final String type; + private final String param; + private final String code; + + public ModelInvokeException(int status, String message, String type, String param, String code) { + super(message); + this.status = status; + this.type = type; + this.param = param; + this.code = code; + } + + public int getStatus() { + return status; + } + + public String getType() { + return type; + } + + public String getParam() { + return param; + } + + public String getCode() { + return code; + } + + public static ModelInvokeException badRequest(String message) { + return badRequest(message, null, "invalid_request_error"); + } + + public static ModelInvokeException badRequest(String message, String param, String code) { + return new ModelInvokeException(400, message, "invalid_request_error", param, code); + } + + public static ModelInvokeException unauthorized(String message) { + return new ModelInvokeException(401, message, "authentication_error", null, "unauthorized"); + } + + public static ModelInvokeException forbidden(String message) { + return new ModelInvokeException(403, message, "permission_error", null, "forbidden"); + } + + public static ModelInvokeException notFound(String message) { + return new ModelInvokeException(404, message, "not_found_error", "model", "model_not_found"); + } + + public static ModelInvokeException badGateway(String message) { + return new ModelInvokeException(502, message, "api_error", null, "upstream_bad_gateway"); + } + + public static ModelInvokeException serviceUnavailable(String message) { + return new ModelInvokeException(503, message, "service_unavailable_error", null, "upstream_unavailable"); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapper.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapper.java new file mode 100644 index 0000000..c2a8e45 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapper.java @@ -0,0 +1,651 @@ +package tech.easyflow.ai.invoke.mapper; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.invoke.exception.ModelInvokeException; +import tech.easyflow.ai.invoke.model.UnifiedChatChunk; +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; +import tech.easyflow.ai.invoke.model.UnifiedChoice; +import tech.easyflow.ai.invoke.model.UnifiedContentPart; +import tech.easyflow.ai.invoke.model.UnifiedImageUrl; +import tech.easyflow.ai.invoke.model.UnifiedMessage; +import tech.easyflow.ai.invoke.model.UnifiedResponseFormat; +import tech.easyflow.ai.invoke.model.UnifiedTool; +import tech.easyflow.ai.invoke.model.UnifiedToolCall; +import tech.easyflow.ai.invoke.model.UnifiedToolCallFunction; +import tech.easyflow.ai.invoke.model.UnifiedToolFunction; +import tech.easyflow.ai.invoke.model.UnifiedUsage; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionChunkResponse; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Component +public class OpenAiProtocolMapper { + + private static final Set ROOT_FIELDS = Set.of( + "model", "messages", "stream", "temperature", "top_p", "max_tokens", + "seed", "tools", "tool_choice", "response_format" + ); + private static final Set MESSAGE_FIELDS = Set.of( + "role", "content", "name", "tool_call_id", "tool_calls" + ); + private static final Set CONTENT_PART_FIELDS = Set.of("type", "text", "image_url"); + private static final Set IMAGE_URL_FIELDS = Set.of("url", "detail"); + private static final Set TOOL_FIELDS = Set.of("type", "function"); + private static final Set TOOL_FUNCTION_FIELDS = Set.of("name", "description", "parameters"); + private static final Set RESPONSE_FORMAT_FIELDS = Set.of("type", "json_schema"); + private static final Set TOOL_CHOICE_FIELDS = Set.of("type", "function"); + + private final ObjectMapper objectMapper; + + public OpenAiProtocolMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public OpenAiChatCompletionRequest readRequest(String rawBody) { + JsonNode rootNode; + try { + rootNode = objectMapper.readTree(rawBody); + } catch (JsonProcessingException e) { + throw ModelInvokeException.badRequest("请求体不是合法 JSON", null, "invalid_json"); + } + + if (rootNode == null || !rootNode.isObject()) { + throw ModelInvokeException.badRequest("请求体必须为 JSON 对象", null, "invalid_json"); + } + validateAllowedFields(rootNode, ROOT_FIELDS, null); + validateMessages(rootNode.path("messages")); + validateTools(rootNode.path("tools")); + validateToolChoice(rootNode.path("tool_choice")); + validateResponseFormat(rootNode.path("response_format")); + + try { + OpenAiChatCompletionRequest request = buildRequest(rootNode); + if (StrUtil.isBlank(request.getModel())) { + throw ModelInvokeException.badRequest("model 不能为空", "model", "model_required"); + } + if (request.getMessages() == null || request.getMessages().isEmpty()) { + throw ModelInvokeException.badRequest("messages 不能为空", "messages", "messages_required"); + } + return request; + } catch (IllegalArgumentException e) { + throw ModelInvokeException.badRequest(e.getMessage(), "messages", "invalid_message_content"); + } + } + + private OpenAiChatCompletionRequest buildRequest(JsonNode rootNode) { + OpenAiChatCompletionRequest request = new OpenAiChatCompletionRequest(); + request.setModel(textValue(rootNode, "model")); + request.setMessages(buildMessages(rootNode.path("messages"))); + if (rootNode.has("stream") && !rootNode.get("stream").isNull()) { + request.setStream(rootNode.get("stream").asBoolean()); + } + if (rootNode.has("temperature") && !rootNode.get("temperature").isNull()) { + request.setTemperature(rootNode.get("temperature").asDouble()); + } + if (rootNode.has("top_p") && !rootNode.get("top_p").isNull()) { + request.setTopP(rootNode.get("top_p").asDouble()); + } + if (rootNode.has("max_tokens") && !rootNode.get("max_tokens").isNull()) { + request.setMaxTokens(rootNode.get("max_tokens").asInt()); + } + if (rootNode.has("seed") && !rootNode.get("seed").isNull()) { + request.setSeed(rootNode.get("seed").asLong()); + } + if (rootNode.has("tools") && rootNode.get("tools").isArray()) { + request.setTools(objectMapper.convertValue( + rootNode.get("tools"), + new TypeReference<>() { + } + )); + } + if (rootNode.has("tool_choice") && !rootNode.get("tool_choice").isNull()) { + request.setToolChoice(rootNode.get("tool_choice")); + } + if (rootNode.has("response_format") && rootNode.get("response_format").isObject()) { + request.setResponseFormat(objectMapper.convertValue( + rootNode.get("response_format"), + OpenAiChatCompletionRequest.ResponseFormat.class + )); + } + return request; + } + + private List buildMessages(JsonNode messagesNode) { + List messages = new ArrayList<>(); + for (JsonNode messageNode : messagesNode) { + OpenAiChatCompletionRequest.Message message = new OpenAiChatCompletionRequest.Message(); + message.setRole(textValue(messageNode, "role")); + if (messageNode.has("content")) { + message.setContentNode(messageNode.get("content")); + } + message.setName(textValue(messageNode, "name")); + message.setToolCallId(textValue(messageNode, "tool_call_id")); + if (messageNode.has("tool_calls") && messageNode.get("tool_calls").isArray()) { + message.setToolCalls(objectMapper.convertValue( + messageNode.get("tool_calls"), + new TypeReference<>() { + } + )); + } + messages.add(message); + } + return messages; + } + + public UnifiedChatRequest toUnifiedRequest(OpenAiChatCompletionRequest request) { + UnifiedChatRequest unifiedRequest = new UnifiedChatRequest(); + unifiedRequest.setModel(request.getModel()); + unifiedRequest.setMessages(toUnifiedMessages(request.getMessages())); + unifiedRequest.setStream(Boolean.TRUE.equals(request.getStream())); + unifiedRequest.setTemperature(request.getTemperature()); + unifiedRequest.setTopP(request.getTopP()); + unifiedRequest.setMaxTokens(request.getMaxTokens()); + unifiedRequest.setSeed(request.getSeed()); + unifiedRequest.setTools(toUnifiedTools(request.getTools())); + unifiedRequest.setToolChoice(request.getToolChoice()); + unifiedRequest.setResponseFormat(toUnifiedResponseFormat(request.getResponseFormat())); + return unifiedRequest; + } + + public OpenAiChatCompletionRequest toOpenAiRequest(UnifiedChatRequest request) { + OpenAiChatCompletionRequest openAiRequest = new OpenAiChatCompletionRequest(); + openAiRequest.setModel(request.getModel()); + openAiRequest.setMessages(toOpenAiMessages(request.getMessages())); + openAiRequest.setStream(request.getStream()); + openAiRequest.setTemperature(request.getTemperature()); + openAiRequest.setTopP(request.getTopP()); + openAiRequest.setMaxTokens(request.getMaxTokens()); + openAiRequest.setSeed(request.getSeed()); + openAiRequest.setTools(toOpenAiTools(request.getTools())); + openAiRequest.setToolChoice(request.getToolChoice()); + openAiRequest.setResponseFormat(toOpenAiResponseFormat(request.getResponseFormat())); + return openAiRequest; + } + + public UnifiedChatResponse toUnifiedResponse(OpenAiChatCompletionResponse response) { + UnifiedChatResponse unifiedResponse = new UnifiedChatResponse(); + unifiedResponse.setId(response.getId()); + unifiedResponse.setObject(response.getObject()); + unifiedResponse.setCreated(response.getCreated()); + unifiedResponse.setModel(response.getModel()); + unifiedResponse.setChoices(toUnifiedChoices(response.getChoices(), false)); + unifiedResponse.setUsage(toUnifiedUsage(response.getUsage())); + return unifiedResponse; + } + + public UnifiedChatChunk toUnifiedChunk(OpenAiChatCompletionChunkResponse chunk) { + UnifiedChatChunk unifiedChunk = new UnifiedChatChunk(); + unifiedChunk.setId(chunk.getId()); + unifiedChunk.setObject(chunk.getObject()); + unifiedChunk.setCreated(chunk.getCreated()); + unifiedChunk.setModel(chunk.getModel()); + unifiedChunk.setChoices(toUnifiedChunkChoices(chunk.getChoices())); + unifiedChunk.setUsage(toUnifiedUsage(chunk.getUsage())); + return unifiedChunk; + } + + public OpenAiChatCompletionResponse toOpenAiResponse(UnifiedChatResponse response) { + OpenAiChatCompletionResponse openAiResponse = new OpenAiChatCompletionResponse(); + openAiResponse.setId(response.getId()); + openAiResponse.setObject(response.getObject()); + openAiResponse.setCreated(response.getCreated()); + openAiResponse.setModel(response.getModel()); + openAiResponse.setChoices(toOpenAiChoices(response.getChoices(), false)); + openAiResponse.setUsage(toOpenAiUsage(response.getUsage())); + return openAiResponse; + } + + public OpenAiChatCompletionChunkResponse toOpenAiChunk(UnifiedChatChunk chunk) { + OpenAiChatCompletionChunkResponse openAiChunk = new OpenAiChatCompletionChunkResponse(); + openAiChunk.setId(chunk.getId()); + openAiChunk.setObject(chunk.getObject()); + openAiChunk.setCreated(chunk.getCreated()); + openAiChunk.setModel(chunk.getModel()); + openAiChunk.setChoices(toOpenAiChunkChoices(chunk.getChoices())); + openAiChunk.setUsage(toOpenAiUsage(chunk.getUsage())); + return openAiChunk; + } + + private void validateMessages(JsonNode messagesNode) { + if (messagesNode == null || !messagesNode.isArray() || messagesNode.isEmpty()) { + throw ModelInvokeException.badRequest("messages 必须为非空数组", "messages", "messages_required"); + } + for (int i = 0; i < messagesNode.size(); i++) { + JsonNode messageNode = messagesNode.get(i); + if (!messageNode.isObject()) { + throw ModelInvokeException.badRequest("messages[" + i + "] 必须为对象", "messages", "invalid_message"); + } + validateAllowedFields(messageNode, MESSAGE_FIELDS, "messages[" + i + "]"); + String role = textValue(messageNode, "role"); + if (!Set.of("system", "developer", "user", "assistant", "tool").contains(role)) { + throw ModelInvokeException.badRequest("messages[" + i + "].role 不受支持", "messages", "invalid_message_role"); + } + JsonNode contentNode = messageNode.get("content"); + if (contentNode != null && !contentNode.isNull() && !contentNode.isTextual() && !contentNode.isArray()) { + throw ModelInvokeException.badRequest("messages[" + i + "].content 仅支持字符串或数组", "messages", "invalid_message_content"); + } + if (contentNode != null && contentNode.isArray()) { + validateContentParts(contentNode, "messages[" + i + "].content"); + } + } + } + + private void validateContentParts(JsonNode contentNode, String path) { + for (int i = 0; i < contentNode.size(); i++) { + JsonNode partNode = contentNode.get(i); + if (!partNode.isObject()) { + throw ModelInvokeException.badRequest(path + "[" + i + "] 必须为对象", "messages", "invalid_content_part"); + } + validateAllowedFields(partNode, CONTENT_PART_FIELDS, path + "[" + i + "]"); + String type = textValue(partNode, "type"); + if (!Set.of("text", "image_url").contains(type)) { + throw ModelInvokeException.badRequest(path + "[" + i + "].type 不受支持", "messages", "content_part_not_supported"); + } + if ("text".equals(type) && StrUtil.isBlank(textValue(partNode, "text"))) { + throw ModelInvokeException.badRequest(path + "[" + i + "].text 不能为空", "messages", "content_text_required"); + } + if ("image_url".equals(type)) { + JsonNode imageUrlNode = partNode.get("image_url"); + if (imageUrlNode == null || !imageUrlNode.isObject()) { + throw ModelInvokeException.badRequest(path + "[" + i + "].image_url 必须为对象", "messages", "image_url_required"); + } + validateAllowedFields(imageUrlNode, IMAGE_URL_FIELDS, path + "[" + i + "].image_url"); + if (StrUtil.isBlank(textValue(imageUrlNode, "url"))) { + throw ModelInvokeException.badRequest(path + "[" + i + "].image_url.url 不能为空", "messages", "image_url_required"); + } + } + } + } + + private void validateTools(JsonNode toolsNode) { + if (isAbsentNode(toolsNode)) { + return; + } + if (!toolsNode.isArray()) { + throw ModelInvokeException.badRequest("tools 必须为数组", "tools", "invalid_tools"); + } + for (int i = 0; i < toolsNode.size(); i++) { + JsonNode toolNode = toolsNode.get(i); + if (!toolNode.isObject()) { + throw ModelInvokeException.badRequest("tools[" + i + "] 必须为对象", "tools", "invalid_tools"); + } + validateAllowedFields(toolNode, TOOL_FIELDS, "tools[" + i + "]"); + JsonNode functionNode = toolNode.get("function"); + if (functionNode == null || !functionNode.isObject()) { + throw ModelInvokeException.badRequest("tools[" + i + "].function 必须为对象", "tools", "invalid_tool_function"); + } + validateAllowedFields(functionNode, TOOL_FUNCTION_FIELDS, "tools[" + i + "].function"); + if (StrUtil.isBlank(textValue(functionNode, "name"))) { + throw ModelInvokeException.badRequest("tools[" + i + "].function.name 不能为空", "tools", "tool_name_required"); + } + } + } + + private void validateToolChoice(JsonNode toolChoiceNode) { + if (isAbsentNode(toolChoiceNode)) { + return; + } + if (toolChoiceNode.isTextual()) { + return; + } + if (!toolChoiceNode.isObject()) { + throw ModelInvokeException.badRequest("tool_choice 仅支持字符串或对象", "tool_choice", "invalid_tool_choice"); + } + validateAllowedFields(toolChoiceNode, TOOL_CHOICE_FIELDS, "tool_choice"); + } + + private void validateResponseFormat(JsonNode responseFormatNode) { + if (isAbsentNode(responseFormatNode)) { + return; + } + if (!responseFormatNode.isObject()) { + throw ModelInvokeException.badRequest("response_format 必须为对象", "response_format", "invalid_response_format"); + } + validateAllowedFields(responseFormatNode, RESPONSE_FORMAT_FIELDS, "response_format"); + if (StrUtil.isBlank(textValue(responseFormatNode, "type"))) { + throw ModelInvokeException.badRequest("response_format.type 不能为空", "response_format", "response_format_type_required"); + } + } + + private boolean isAbsentNode(JsonNode node) { + return node == null || node.isNull() || node.isMissingNode(); + } + + private void validateAllowedFields(JsonNode node, Set allowedFields, String path) { + Iterator fieldNames = node.fieldNames(); + Set unsupportedFields = new LinkedHashSet<>(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + if (!allowedFields.contains(fieldName)) { + unsupportedFields.add(path == null ? fieldName : path + "." + fieldName); + } + } + if (!unsupportedFields.isEmpty()) { + String field = unsupportedFields.iterator().next(); + throw ModelInvokeException.badRequest("存在未支持字段: " + field, field, "unsupported_field"); + } + } + + private String textValue(JsonNode node, String fieldName) { + JsonNode fieldNode = node.get(fieldName); + if (fieldNode == null || fieldNode.isNull()) { + return null; + } + return fieldNode.asText(); + } + + private List toUnifiedMessages(List messages) { + if (messages == null) { + return null; + } + List result = new ArrayList<>(); + for (OpenAiChatCompletionRequest.Message message : messages) { + UnifiedMessage unifiedMessage = new UnifiedMessage(); + unifiedMessage.setRole(message.getRole()); + unifiedMessage.setContent(message.getContent()); + unifiedMessage.setContentParts(toUnifiedContentParts(message.getContentParts())); + unifiedMessage.setName(message.getName()); + unifiedMessage.setToolCallId(message.getToolCallId()); + unifiedMessage.setToolCalls(toUnifiedToolCalls(message.getToolCalls())); + result.add(unifiedMessage); + } + return result; + } + + private List toOpenAiMessages(List messages) { + if (messages == null) { + return null; + } + List result = new ArrayList<>(); + for (UnifiedMessage message : messages) { + OpenAiChatCompletionRequest.Message openAiMessage = new OpenAiChatCompletionRequest.Message(); + openAiMessage.setRole(message.getRole()); + if (message.getContentParts() != null && !message.getContentParts().isEmpty()) { + ObjectNode contentNode = objectMapper.createObjectNode(); + contentNode.set("content", objectMapper.valueToTree(toOpenAiContentParts(message.getContentParts()))); + openAiMessage.setContentNode(contentNode.get("content")); + } else { + ObjectNode contentNode = objectMapper.createObjectNode(); + if (message.getContent() == null) { + contentNode.putNull("content"); + } else { + contentNode.put("content", message.getContent()); + } + openAiMessage.setContentNode(contentNode.get("content")); + } + openAiMessage.setName(message.getName()); + openAiMessage.setToolCallId(message.getToolCallId()); + openAiMessage.setToolCalls(toOpenAiToolCalls(message.getToolCalls())); + result.add(openAiMessage); + } + return result; + } + + private List toUnifiedContentParts(List contentParts) { + if (contentParts == null) { + return null; + } + List result = new ArrayList<>(); + for (OpenAiChatCompletionRequest.ContentPart contentPart : contentParts) { + UnifiedContentPart unifiedContentPart = new UnifiedContentPart(); + unifiedContentPart.setType(contentPart.getType()); + unifiedContentPart.setText(contentPart.getText()); + if (contentPart.getImageUrl() != null) { + UnifiedImageUrl imageUrl = new UnifiedImageUrl(); + imageUrl.setUrl(contentPart.getImageUrl().getUrl()); + imageUrl.setDetail(contentPart.getImageUrl().getDetail()); + unifiedContentPart.setImageUrl(imageUrl); + } + result.add(unifiedContentPart); + } + return result; + } + + private List toOpenAiContentParts(List contentParts) { + if (contentParts == null) { + return null; + } + List result = new ArrayList<>(); + for (UnifiedContentPart contentPart : contentParts) { + OpenAiChatCompletionRequest.ContentPart openAiPart = new OpenAiChatCompletionRequest.ContentPart(); + openAiPart.setType(contentPart.getType()); + openAiPart.setText(contentPart.getText()); + if (contentPart.getImageUrl() != null) { + OpenAiChatCompletionRequest.ImageUrl imageUrl = new OpenAiChatCompletionRequest.ImageUrl(); + imageUrl.setUrl(contentPart.getImageUrl().getUrl()); + imageUrl.setDetail(contentPart.getImageUrl().getDetail()); + openAiPart.setImageUrl(imageUrl); + } + result.add(openAiPart); + } + return result; + } + + private List toUnifiedTools(List tools) { + if (tools == null) { + return null; + } + List result = new ArrayList<>(); + for (OpenAiChatCompletionRequest.Tool tool : tools) { + UnifiedTool unifiedTool = new UnifiedTool(); + unifiedTool.setType(tool.getType()); + if (tool.getFunction() != null) { + UnifiedToolFunction function = new UnifiedToolFunction(); + function.setName(tool.getFunction().getName()); + function.setDescription(tool.getFunction().getDescription()); + function.setParameters(tool.getFunction().getParameters()); + unifiedTool.setFunction(function); + } + result.add(unifiedTool); + } + return result; + } + + private List toOpenAiTools(List tools) { + if (tools == null) { + return null; + } + List result = new ArrayList<>(); + for (UnifiedTool tool : tools) { + OpenAiChatCompletionRequest.Tool openAiTool = new OpenAiChatCompletionRequest.Tool(); + openAiTool.setType(tool.getType()); + if (tool.getFunction() != null) { + OpenAiChatCompletionRequest.ToolFunction function = new OpenAiChatCompletionRequest.ToolFunction(); + function.setName(tool.getFunction().getName()); + function.setDescription(tool.getFunction().getDescription()); + function.setParameters(tool.getFunction().getParameters()); + openAiTool.setFunction(function); + } + result.add(openAiTool); + } + return result; + } + + private UnifiedResponseFormat toUnifiedResponseFormat(OpenAiChatCompletionRequest.ResponseFormat responseFormat) { + if (responseFormat == null) { + return null; + } + UnifiedResponseFormat unifiedResponseFormat = new UnifiedResponseFormat(); + unifiedResponseFormat.setType(responseFormat.getType()); + unifiedResponseFormat.setJsonSchema(responseFormat.getJsonSchema()); + return unifiedResponseFormat; + } + + private OpenAiChatCompletionRequest.ResponseFormat toOpenAiResponseFormat(UnifiedResponseFormat responseFormat) { + if (responseFormat == null) { + return null; + } + OpenAiChatCompletionRequest.ResponseFormat openAiResponseFormat = new OpenAiChatCompletionRequest.ResponseFormat(); + openAiResponseFormat.setType(responseFormat.getType()); + openAiResponseFormat.setJsonSchema(responseFormat.getJsonSchema()); + return openAiResponseFormat; + } + + private List toUnifiedChoices(List choices, boolean delta) { + if (choices == null) { + return null; + } + List result = new ArrayList<>(); + for (OpenAiChatCompletionResponse.Choice choice : choices) { + UnifiedChoice unifiedChoice = new UnifiedChoice(); + unifiedChoice.setIndex(choice.getIndex()); + UnifiedMessage message = new UnifiedMessage(); + if (choice.getMessage() != null) { + message.setRole(choice.getMessage().getRole()); + message.setContent(choice.getMessage().getContent()); + message.setToolCallId(choice.getMessage().getToolCallId()); + message.setToolCalls(toUnifiedToolCalls(choice.getMessage().getToolCalls())); + } + if (delta) { + unifiedChoice.setDelta(message); + } else { + unifiedChoice.setMessage(message); + } + unifiedChoice.setFinishReason(choice.getFinishReason()); + result.add(unifiedChoice); + } + return result; + } + + private List toUnifiedChunkChoices(List choices) { + if (choices == null) { + return null; + } + List result = new ArrayList<>(); + for (OpenAiChatCompletionChunkResponse.Choice choice : choices) { + UnifiedChoice unifiedChoice = new UnifiedChoice(); + unifiedChoice.setIndex(choice.getIndex()); + UnifiedMessage delta = new UnifiedMessage(); + if (choice.getDelta() != null) { + delta.setRole(choice.getDelta().getRole()); + delta.setContent(choice.getDelta().getContent()); + delta.setToolCalls(toUnifiedToolCalls(choice.getDelta().getToolCalls())); + } + unifiedChoice.setDelta(delta); + unifiedChoice.setFinishReason(choice.getFinishReason()); + result.add(unifiedChoice); + } + return result; + } + + private List toOpenAiChoices(List choices, boolean delta) { + if (choices == null) { + return null; + } + List result = new ArrayList<>(); + for (UnifiedChoice choice : choices) { + OpenAiChatCompletionResponse.Choice openAiChoice = new OpenAiChatCompletionResponse.Choice(); + openAiChoice.setIndex(choice.getIndex()); + UnifiedMessage source = delta ? choice.getDelta() : choice.getMessage(); + if (source != null) { + OpenAiChatCompletionResponse.Message message = new OpenAiChatCompletionResponse.Message(); + message.setRole(source.getRole()); + message.setContent(source.getContent()); + message.setToolCallId(source.getToolCallId()); + message.setToolCalls(toOpenAiToolCalls(source.getToolCalls())); + openAiChoice.setMessage(message); + } + openAiChoice.setFinishReason(choice.getFinishReason()); + result.add(openAiChoice); + } + return result; + } + + private List toOpenAiChunkChoices(List choices) { + if (choices == null) { + return null; + } + List result = new ArrayList<>(); + for (UnifiedChoice choice : choices) { + OpenAiChatCompletionChunkResponse.Choice openAiChoice = new OpenAiChatCompletionChunkResponse.Choice(); + openAiChoice.setIndex(choice.getIndex()); + if (choice.getDelta() != null) { + OpenAiChatCompletionChunkResponse.Delta delta = new OpenAiChatCompletionChunkResponse.Delta(); + delta.setRole(choice.getDelta().getRole()); + delta.setContent(choice.getDelta().getContent()); + delta.setToolCalls(toOpenAiToolCalls(choice.getDelta().getToolCalls())); + openAiChoice.setDelta(delta); + } + openAiChoice.setFinishReason(choice.getFinishReason()); + result.add(openAiChoice); + } + return result; + } + + private List toUnifiedToolCalls(List toolCalls) { + if (toolCalls == null) { + return null; + } + List result = new ArrayList<>(); + for (OpenAiChatCompletionResponse.ToolCall toolCall : toolCalls) { + UnifiedToolCall unifiedToolCall = new UnifiedToolCall(); + unifiedToolCall.setIndex(toolCall.getIndex()); + unifiedToolCall.setId(toolCall.getId()); + unifiedToolCall.setType(toolCall.getType()); + if (toolCall.getFunction() != null) { + UnifiedToolCallFunction function = new UnifiedToolCallFunction(); + function.setName(toolCall.getFunction().getName()); + function.setArguments(toolCall.getFunction().getArguments()); + unifiedToolCall.setFunction(function); + } + result.add(unifiedToolCall); + } + return result; + } + + private List toOpenAiToolCalls(List toolCalls) { + if (toolCalls == null) { + return null; + } + List result = new ArrayList<>(); + for (UnifiedToolCall toolCall : toolCalls) { + OpenAiChatCompletionResponse.ToolCall openAiToolCall = new OpenAiChatCompletionResponse.ToolCall(); + openAiToolCall.setIndex(toolCall.getIndex()); + openAiToolCall.setId(toolCall.getId()); + openAiToolCall.setType(toolCall.getType()); + if (toolCall.getFunction() != null) { + OpenAiChatCompletionResponse.ToolCallFunction function = new OpenAiChatCompletionResponse.ToolCallFunction(); + function.setName(toolCall.getFunction().getName()); + function.setArguments(toolCall.getFunction().getArguments()); + openAiToolCall.setFunction(function); + } + result.add(openAiToolCall); + } + return result; + } + + private UnifiedUsage toUnifiedUsage(OpenAiChatCompletionResponse.Usage usage) { + if (usage == null) { + return null; + } + UnifiedUsage unifiedUsage = new UnifiedUsage(); + unifiedUsage.setPromptTokens(usage.getPromptTokens()); + unifiedUsage.setCompletionTokens(usage.getCompletionTokens()); + unifiedUsage.setTotalTokens(usage.getTotalTokens()); + return unifiedUsage; + } + + private OpenAiChatCompletionResponse.Usage toOpenAiUsage(UnifiedUsage usage) { + if (usage == null) { + return null; + } + OpenAiChatCompletionResponse.Usage openAiUsage = new OpenAiChatCompletionResponse.Usage(); + openAiUsage.setPromptTokens(usage.getPromptTokens()); + openAiUsage.setCompletionTokens(usage.getCompletionTokens()); + openAiUsage.setTotalTokens(usage.getTotalTokens()); + return openAiUsage; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatChunk.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatChunk.java new file mode 100644 index 0000000..422058f --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatChunk.java @@ -0,0 +1,61 @@ +package tech.easyflow.ai.invoke.model; + +import java.util.List; + +public class UnifiedChatChunk { + + private String id; + private String object; + private Long created; + private String model; + private List choices; + private UnifiedUsage usage; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public UnifiedUsage getUsage() { + return usage; + } + + public void setUsage(UnifiedUsage usage) { + this.usage = usage; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatRequest.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatRequest.java new file mode 100644 index 0000000..0f5c872 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatRequest.java @@ -0,0 +1,99 @@ +package tech.easyflow.ai.invoke.model; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.List; + +public class UnifiedChatRequest { + + private String model; + private List messages; + private Boolean stream; + private Double temperature; + private Double topP; + private Integer maxTokens; + private Long seed; + private List tools; + private JsonNode toolChoice; + private UnifiedResponseFormat responseFormat; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } + + public Boolean getStream() { + return stream; + } + + public void setStream(Boolean stream) { + this.stream = stream; + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public Double getTopP() { + return topP; + } + + public void setTopP(Double topP) { + this.topP = topP; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + public Long getSeed() { + return seed; + } + + public void setSeed(Long seed) { + this.seed = seed; + } + + public List getTools() { + return tools; + } + + public void setTools(List tools) { + this.tools = tools; + } + + public JsonNode getToolChoice() { + return toolChoice; + } + + public void setToolChoice(JsonNode toolChoice) { + this.toolChoice = toolChoice; + } + + public UnifiedResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setResponseFormat(UnifiedResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatResponse.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatResponse.java new file mode 100644 index 0000000..a88e341 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChatResponse.java @@ -0,0 +1,61 @@ +package tech.easyflow.ai.invoke.model; + +import java.util.List; + +public class UnifiedChatResponse { + + private String id; + private String object; + private Long created; + private String model; + private List choices; + private UnifiedUsage usage; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public UnifiedUsage getUsage() { + return usage; + } + + public void setUsage(UnifiedUsage usage) { + this.usage = usage; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChoice.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChoice.java new file mode 100644 index 0000000..b479167 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedChoice.java @@ -0,0 +1,41 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedChoice { + + private Integer index; + private UnifiedMessage message; + private UnifiedMessage delta; + private String finishReason; + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public UnifiedMessage getMessage() { + return message; + } + + public void setMessage(UnifiedMessage message) { + this.message = message; + } + + public UnifiedMessage getDelta() { + return delta; + } + + public void setDelta(UnifiedMessage delta) { + this.delta = delta; + } + + public String getFinishReason() { + return finishReason; + } + + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedContentPart.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedContentPart.java new file mode 100644 index 0000000..f6cf6ea --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedContentPart.java @@ -0,0 +1,32 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedContentPart { + + private String type; + private String text; + private UnifiedImageUrl imageUrl; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public UnifiedImageUrl getImageUrl() { + return imageUrl; + } + + public void setImageUrl(UnifiedImageUrl imageUrl) { + this.imageUrl = imageUrl; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedImageUrl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedImageUrl.java new file mode 100644 index 0000000..0c90ba0 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedImageUrl.java @@ -0,0 +1,23 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedImageUrl { + + private String url; + private String detail; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDetail() { + return detail; + } + + public void setDetail(String detail) { + this.detail = detail; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedMessage.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedMessage.java new file mode 100644 index 0000000..122c8d3 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedMessage.java @@ -0,0 +1,61 @@ +package tech.easyflow.ai.invoke.model; + +import java.util.List; + +public class UnifiedMessage { + + private String role; + private String content; + private List contentParts; + private String name; + private String toolCallId; + private List toolCalls; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public List getContentParts() { + return contentParts; + } + + public void setContentParts(List contentParts) { + this.contentParts = contentParts; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public List getToolCalls() { + return toolCalls; + } + + public void setToolCalls(List toolCalls) { + this.toolCalls = toolCalls; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedResponseFormat.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedResponseFormat.java new file mode 100644 index 0000000..4fd434f --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedResponseFormat.java @@ -0,0 +1,25 @@ +package tech.easyflow.ai.invoke.model; + +import com.fasterxml.jackson.databind.JsonNode; + +public class UnifiedResponseFormat { + + private String type; + private JsonNode jsonSchema; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public JsonNode getJsonSchema() { + return jsonSchema; + } + + public void setJsonSchema(JsonNode jsonSchema) { + this.jsonSchema = jsonSchema; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedTool.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedTool.java new file mode 100644 index 0000000..cd90bd5 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedTool.java @@ -0,0 +1,23 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedTool { + + private String type; + private UnifiedToolFunction function; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public UnifiedToolFunction getFunction() { + return function; + } + + public void setFunction(UnifiedToolFunction function) { + this.function = function; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCall.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCall.java new file mode 100644 index 0000000..1d0cee6 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCall.java @@ -0,0 +1,41 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedToolCall { + + private Integer index; + private String id; + private String type; + private UnifiedToolCallFunction function; + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public UnifiedToolCallFunction getFunction() { + return function; + } + + public void setFunction(UnifiedToolCallFunction function) { + this.function = function; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCallFunction.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCallFunction.java new file mode 100644 index 0000000..251aea2 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolCallFunction.java @@ -0,0 +1,23 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedToolCallFunction { + + private String name; + private String arguments; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getArguments() { + return arguments; + } + + public void setArguments(String arguments) { + this.arguments = arguments; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolFunction.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolFunction.java new file mode 100644 index 0000000..8080eb5 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedToolFunction.java @@ -0,0 +1,34 @@ +package tech.easyflow.ai.invoke.model; + +import com.fasterxml.jackson.databind.JsonNode; + +public class UnifiedToolFunction { + + private String name; + private String description; + private JsonNode parameters; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public JsonNode getParameters() { + return parameters; + } + + public void setParameters(JsonNode parameters) { + this.parameters = parameters; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedUsage.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedUsage.java new file mode 100644 index 0000000..73ffe75 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/model/UnifiedUsage.java @@ -0,0 +1,32 @@ +package tech.easyflow.ai.invoke.model; + +public class UnifiedUsage { + + private Integer promptTokens; + private Integer completionTokens; + private Integer totalTokens; + + public Integer getPromptTokens() { + return promptTokens; + } + + public void setPromptTokens(Integer promptTokens) { + this.promptTokens = promptTokens; + } + + public Integer getCompletionTokens() { + return completionTokens; + } + + public void setCompletionTokens(Integer completionTokens) { + this.completionTokens = completionTokens; + } + + public Integer getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(Integer totalTokens) { + this.totalTokens = totalTokens; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionChunkResponse.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionChunkResponse.java new file mode 100644 index 0000000..8bff06c --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionChunkResponse.java @@ -0,0 +1,134 @@ +package tech.easyflow.ai.invoke.protocol.openai; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OpenAiChatCompletionChunkResponse { + + private String id; + private String object; + private Long created; + private String model; + private List choices; + private OpenAiChatCompletionResponse.Usage usage; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public OpenAiChatCompletionResponse.Usage getUsage() { + return usage; + } + + public void setUsage(OpenAiChatCompletionResponse.Usage usage) { + this.usage = usage; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Choice { + + private Integer index; + private Delta delta; + @JSONField(name = "finish_reason") + @JsonProperty("finish_reason") + private String finishReason; + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public Delta getDelta() { + return delta; + } + + public void setDelta(Delta delta) { + this.delta = delta; + } + + public String getFinishReason() { + return finishReason; + } + + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Delta { + + private String role; + private String content; + @JSONField(name = "tool_calls") + @JsonProperty("tool_calls") + private List toolCalls; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public List getToolCalls() { + return toolCalls; + } + + public void setToolCalls(List toolCalls) { + this.toolCalls = toolCalls; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionRequest.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionRequest.java new file mode 100644 index 0000000..b9e3576 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionRequest.java @@ -0,0 +1,335 @@ +package tech.easyflow.ai.invoke.protocol.openai; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OpenAiChatCompletionRequest { + + private String model; + private List messages; + private Boolean stream; + private Double temperature; + @JSONField(name = "top_p") + @JsonProperty("top_p") + private Double topP; + @JSONField(name = "max_tokens") + @JsonProperty("max_tokens") + private Integer maxTokens; + private Long seed; + private List tools; + @JSONField(name = "tool_choice") + @JsonProperty("tool_choice") + private JsonNode toolChoice; + @JSONField(name = "response_format") + @JsonProperty("response_format") + private ResponseFormat responseFormat; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } + + public Boolean getStream() { + return stream; + } + + public void setStream(Boolean stream) { + this.stream = stream; + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public Double getTopP() { + return topP; + } + + public void setTopP(Double topP) { + this.topP = topP; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + public Long getSeed() { + return seed; + } + + public void setSeed(Long seed) { + this.seed = seed; + } + + public List getTools() { + return tools; + } + + public void setTools(List tools) { + this.tools = tools; + } + + public JsonNode getToolChoice() { + return toolChoice; + } + + public void setToolChoice(JsonNode toolChoice) { + this.toolChoice = toolChoice; + } + + public ResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setResponseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Message { + + private String role; + private String content; + private List contentParts; + private String name; + @JSONField(name = "tool_call_id") + @JsonProperty("tool_call_id") + private String toolCallId; + @JSONField(name = "tool_calls") + @JsonProperty("tool_calls") + private List toolCalls; + + @JsonProperty("content") + public void setContentNode(JsonNode contentNode) { + if (contentNode == null || contentNode.isNull()) { + this.content = null; + this.contentParts = null; + return; + } + if (contentNode.isTextual()) { + this.content = contentNode.asText(); + this.contentParts = null; + return; + } + if (contentNode.isArray()) { + this.content = null; + this.contentParts = OpenAiJsonSupport.convertContentParts(contentNode); + return; + } + throw new IllegalArgumentException("message.content 仅支持字符串或数组"); + } + + @JSONField(name = "content") + @JsonProperty("content") + public Object getContentNode() { + if (contentParts != null) { + return contentParts; + } + return content; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + @JsonIgnore + public String getContent() { + return content; + } + + @JsonIgnore + public List getContentParts() { + return contentParts; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public List getToolCalls() { + return toolCalls; + } + + public void setToolCalls(List toolCalls) { + this.toolCalls = toolCalls; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ContentPart { + + private String type; + private String text; + @JSONField(name = "image_url") + @JsonProperty("image_url") + private ImageUrl imageUrl; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public ImageUrl getImageUrl() { + return imageUrl; + } + + public void setImageUrl(ImageUrl imageUrl) { + this.imageUrl = imageUrl; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ImageUrl { + + private String url; + private String detail; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDetail() { + return detail; + } + + public void setDetail(String detail) { + this.detail = detail; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Tool { + + private String type; + private ToolFunction function; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public ToolFunction getFunction() { + return function; + } + + public void setFunction(ToolFunction function) { + this.function = function; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolFunction { + + private String name; + private String description; + private JsonNode parameters; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public JsonNode getParameters() { + return parameters; + } + + public void setParameters(JsonNode parameters) { + this.parameters = parameters; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ResponseFormat { + + private String type; + @JSONField(name = "json_schema") + @JsonProperty("json_schema") + private JsonNode jsonSchema; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public JsonNode getJsonSchema() { + return jsonSchema; + } + + public void setJsonSchema(JsonNode jsonSchema) { + this.jsonSchema = jsonSchema; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionResponse.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionResponse.java new file mode 100644 index 0000000..ea2b49b --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiChatCompletionResponse.java @@ -0,0 +1,247 @@ +package tech.easyflow.ai.invoke.protocol.openai; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OpenAiChatCompletionResponse { + + private String id; + private String object; + private Long created; + private String model; + private List choices; + private Usage usage; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public Usage getUsage() { + return usage; + } + + public void setUsage(Usage usage) { + this.usage = usage; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Choice { + + private Integer index; + private Message message; + @JSONField(name = "finish_reason") + @JsonProperty("finish_reason") + private String finishReason; + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public Message getMessage() { + return message; + } + + public void setMessage(Message message) { + this.message = message; + } + + public String getFinishReason() { + return finishReason; + } + + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Message { + + private String role; + private String content; + @JSONField(name = "tool_call_id") + @JsonProperty("tool_call_id") + private String toolCallId; + @JSONField(name = "tool_calls") + @JsonProperty("tool_calls") + private List toolCalls; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getToolCallId() { + return toolCallId; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public List getToolCalls() { + return toolCalls; + } + + public void setToolCalls(List toolCalls) { + this.toolCalls = toolCalls; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolCall { + + private Integer index; + private String id; + private String type; + private ToolCallFunction function; + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public ToolCallFunction getFunction() { + return function; + } + + public void setFunction(ToolCallFunction function) { + this.function = function; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ToolCallFunction { + + private String name; + private String arguments; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getArguments() { + return arguments; + } + + public void setArguments(String arguments) { + this.arguments = arguments; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Usage { + + @JSONField(name = "prompt_tokens") + @JsonProperty("prompt_tokens") + private Integer promptTokens; + @JSONField(name = "completion_tokens") + @JsonProperty("completion_tokens") + private Integer completionTokens; + @JSONField(name = "total_tokens") + @JsonProperty("total_tokens") + private Integer totalTokens; + + public Integer getPromptTokens() { + return promptTokens; + } + + public void setPromptTokens(Integer promptTokens) { + this.promptTokens = promptTokens; + } + + public Integer getCompletionTokens() { + return completionTokens; + } + + public void setCompletionTokens(Integer completionTokens) { + this.completionTokens = completionTokens; + } + + public Integer getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(Integer totalTokens) { + this.totalTokens = totalTokens; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiErrorResponse.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiErrorResponse.java new file mode 100644 index 0000000..b9ae34b --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiErrorResponse.java @@ -0,0 +1,54 @@ +package tech.easyflow.ai.invoke.protocol.openai; + +public class OpenAiErrorResponse { + + private Error error; + + public Error getError() { + return error; + } + + public void setError(Error error) { + this.error = error; + } + + public static class Error { + + private String message; + private String type; + private String param; + private String code; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getParam() { + return param; + } + + public void setParam(String param) { + this.param = param; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiJsonSupport.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiJsonSupport.java new file mode 100644 index 0000000..fa73eba --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/protocol/openai/OpenAiJsonSupport.java @@ -0,0 +1,20 @@ +package tech.easyflow.ai.invoke.protocol.openai; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; + +final class OpenAiJsonSupport { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private OpenAiJsonSupport() { + } + + static List convertContentParts(JsonNode node) { + return OBJECT_MAPPER.convertValue(node, new TypeReference<>() { + }); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGateway.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGateway.java new file mode 100644 index 0000000..1359e5b --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGateway.java @@ -0,0 +1,14 @@ +package tech.easyflow.ai.invoke.provider; + +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; + +public interface ModelProviderGateway { + + boolean supports(String providerType); + + UnifiedChatResponse chat(Model model, UnifiedChatRequest request); + + void chatStream(Model model, UnifiedChatRequest request, UnifiedChatChunkObserver observer); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGatewayRegistry.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGatewayRegistry.java new file mode 100644 index 0000000..7630374 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/ModelProviderGatewayRegistry.java @@ -0,0 +1,27 @@ +package tech.easyflow.ai.invoke.provider; + +import org.springframework.stereotype.Component; +import tech.easyflow.ai.invoke.exception.ModelInvokeException; + +import java.util.List; + +@Component +public class ModelProviderGatewayRegistry { + + private final List gateways; + + public ModelProviderGatewayRegistry(List gateways) { + this.gateways = gateways; + } + + public ModelProviderGateway getGateway(String providerType) { + return gateways.stream() + .filter(gateway -> gateway.supports(providerType)) + .findFirst() + .orElseThrow(() -> ModelInvokeException.badRequest( + "当前 providerType 暂不支持统一模型调用: " + providerType, + "model", + "provider_not_supported" + )); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/UnifiedChatChunkObserver.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/UnifiedChatChunkObserver.java new file mode 100644 index 0000000..5bdb07a --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/UnifiedChatChunkObserver.java @@ -0,0 +1,14 @@ +package tech.easyflow.ai.invoke.provider; + +import tech.easyflow.ai.invoke.model.UnifiedChatChunk; + +public interface UnifiedChatChunkObserver { + + void onChunk(UnifiedChatChunk chunk); + + default void onComplete() { + } + + default void onError(Throwable throwable) { + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/base/AbstractOpenAiCompatibleGateway.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/base/AbstractOpenAiCompatibleGateway.java new file mode 100644 index 0000000..5316fd5 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/base/AbstractOpenAiCompatibleGateway.java @@ -0,0 +1,129 @@ +package tech.easyflow.ai.invoke.provider.base; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.BufferedSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.invoke.exception.ModelInvokeException; +import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper; +import tech.easyflow.ai.invoke.model.UnifiedChatChunk; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionChunkResponse; +import tech.easyflow.common.util.OkHttpClientUtil; + +import java.io.IOException; +import java.util.Objects; + +public abstract class AbstractOpenAiCompatibleGateway { + + private static final Logger log = LoggerFactory.getLogger(AbstractOpenAiCompatibleGateway.class); + private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8"); + + protected final ObjectMapper objectMapper; + protected final OpenAiProtocolMapper openAiProtocolMapper; + protected final ThreadPoolTaskExecutor sseThreadPool; + + protected AbstractOpenAiCompatibleGateway(ObjectMapper objectMapper, + OpenAiProtocolMapper openAiProtocolMapper, + ThreadPoolTaskExecutor sseThreadPool) { + this.objectMapper = objectMapper; + this.openAiProtocolMapper = openAiProtocolMapper; + this.sseThreadPool = sseThreadPool; + } + + protected Response executePost(Model model, Object requestBody) { + try { + String body = objectMapper.writeValueAsString(requestBody); + Request request = new Request.Builder() + .url(buildUrl(model)) + .addHeader("Authorization", "Bearer " + model.checkAndGetApiKey()) + .addHeader("Content-Type", "application/json") + .post(RequestBody.create(body, JSON_TYPE)) + .build(); + return OkHttpClientUtil.buildDefaultClient().newCall(request).execute(); + } catch (ModelInvokeException e) { + throw e; + } catch (Exception e) { + throw ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage()); + } + } + + protected String buildUrl(Model model) { + return StrUtil.removeSuffix(model.checkAndGetEndpoint(), "/") + + "/" + + StrUtil.removePrefix(model.checkAndGetRequestPath(), "/"); + } + + protected void validateResponse(Response response) throws IOException { + if (response.isSuccessful()) { + return; + } + String message = extractUpstreamErrorMessage(response); + int code = response.code(); + if (code >= 500 || code == 429) { + throw ModelInvokeException.serviceUnavailable(message); + } + throw ModelInvokeException.badGateway(message); + } + + protected String extractUpstreamErrorMessage(Response response) throws IOException { + ResponseBody body = response.body(); + String bodyString = body == null ? "" : body.string(); + if (StrUtil.isBlank(bodyString)) { + return "上游模型调用失败,HTTP " + response.code(); + } + try { + JsonNode root = objectMapper.readTree(bodyString); + String errorMessage = root.path("error").path("message").asText(); + if (StrUtil.isNotBlank(errorMessage)) { + return "上游模型调用失败,HTTP " + response.code() + ": " + errorMessage; + } + } catch (Exception ignored) { + } + return "上游模型调用失败,HTTP " + response.code() + ": " + bodyString; + } + + protected void streamResponse(Response response, + tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver observer) { + sseThreadPool.execute(() -> { + try (Response closeableResponse = response; ResponseBody body = closeableResponse.body()) { + if (body == null) { + throw new IOException("上游流式响应体为空"); + } + BufferedSource source = body.source(); + while (!source.exhausted()) { + String line = source.readUtf8Line(); + if (line == null) { + break; + } + if (line.isBlank() || !line.startsWith("data:")) { + continue; + } + String payload = line.substring(5).trim(); + if (Objects.equals(payload, "[DONE]")) { + observer.onComplete(); + return; + } + OpenAiChatCompletionChunkResponse chunkResponse = objectMapper.readValue( + payload, + OpenAiChatCompletionChunkResponse.class + ); + UnifiedChatChunk chunk = openAiProtocolMapper.toUnifiedChunk(chunkResponse); + observer.onChunk(chunk); + } + observer.onComplete(); + } catch (Exception e) { + log.error("streamResponse error: {}", e.getMessage(), e); + observer.onError(e); + } + }); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/openai/OpenAiGateway.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/openai/OpenAiGateway.java new file mode 100644 index 0000000..a420ad4 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/provider/openai/OpenAiGateway.java @@ -0,0 +1,78 @@ +package tech.easyflow.ai.invoke.provider.openai; + +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.Response; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper; +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse; +import tech.easyflow.ai.invoke.provider.ModelProviderGateway; +import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver; +import tech.easyflow.ai.invoke.provider.base.AbstractOpenAiCompatibleGateway; + +import java.util.Set; + +@Component +public class OpenAiGateway extends AbstractOpenAiCompatibleGateway implements ModelProviderGateway { + + private static final Set OPENAI_COMPATIBLE_PROVIDER_TYPES = Set.of( + "openai", + "deepseek", + "aliyun", + "zhipu", + "minimax", + "kimi", + "siliconlow", + "ollama", + "self-hosted" + ); + + public OpenAiGateway(ObjectMapper objectMapper, + OpenAiProtocolMapper openAiProtocolMapper, + ThreadPoolTaskExecutor sseThreadPool) { + super(objectMapper, openAiProtocolMapper, sseThreadPool); + } + + @Override + public boolean supports(String providerType) { + return providerType != null && OPENAI_COMPATIBLE_PROVIDER_TYPES.contains(providerType.toLowerCase()); + } + + @Override + public UnifiedChatResponse chat(Model model, UnifiedChatRequest request) { + OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.toOpenAiRequest(request); + try (Response response = executePost(model, openAiRequest)) { + validateResponse(response); + if (response.body() == null) { + throw tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("上游模型响应为空"); + } + OpenAiChatCompletionResponse responseBody = objectMapper.readValue( + response.body().string(), + OpenAiChatCompletionResponse.class + ); + return openAiProtocolMapper.toUnifiedResponse(responseBody); + } catch (tech.easyflow.ai.invoke.exception.ModelInvokeException e) { + throw e; + } catch (Exception e) { + throw tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage()); + } + } + + @Override + public void chatStream(Model model, UnifiedChatRequest request, UnifiedChatChunkObserver observer) { + OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.toOpenAiRequest(request); + Response response = executePost(model, openAiRequest); + try { + validateResponse(response); + streamResponse(response, observer); + } catch (RuntimeException | java.io.IOException e) { + response.close(); + throw e instanceof RuntimeException ? (RuntimeException) e + : tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage()); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/UnifiedModelInvokeService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/UnifiedModelInvokeService.java new file mode 100644 index 0000000..81c0e29 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/UnifiedModelInvokeService.java @@ -0,0 +1,12 @@ +package tech.easyflow.ai.invoke.service; + +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; +import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver; + +public interface UnifiedModelInvokeService { + + UnifiedChatResponse chat(UnifiedChatRequest request); + + void chatStream(UnifiedChatRequest request, UnifiedChatChunkObserver observer); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/impl/UnifiedModelInvokeServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/impl/UnifiedModelInvokeServiceImpl.java new file mode 100644 index 0000000..0085d89 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/invoke/service/impl/UnifiedModelInvokeServiceImpl.java @@ -0,0 +1,114 @@ +package tech.easyflow.ai.invoke.service.impl; + +import cn.hutool.core.util.StrUtil; +import org.springframework.stereotype.Service; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.invoke.exception.ModelInvokeException; +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; +import tech.easyflow.ai.invoke.model.UnifiedContentPart; +import tech.easyflow.ai.invoke.model.UnifiedMessage; +import tech.easyflow.ai.invoke.provider.ModelProviderGateway; +import tech.easyflow.ai.invoke.provider.ModelProviderGatewayRegistry; +import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver; +import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService; +import tech.easyflow.ai.service.ModelService; + +import java.util.List; + +@Service +public class UnifiedModelInvokeServiceImpl implements UnifiedModelInvokeService { + + private final ModelService modelService; + private final ModelProviderGatewayRegistry gatewayRegistry; + + public UnifiedModelInvokeServiceImpl(ModelService modelService, + ModelProviderGatewayRegistry gatewayRegistry) { + this.modelService = modelService; + this.gatewayRegistry = gatewayRegistry; + } + + @Override + public UnifiedChatResponse chat(UnifiedChatRequest request) { + Model model = resolveModel(request); + validateRequestAgainstModel(model, request); + ModelProviderGateway gateway = gatewayRegistry.getGateway(model.getModelProvider().getProviderType()); + return gateway.chat(model, request); + } + + @Override + public void chatStream(UnifiedChatRequest request, UnifiedChatChunkObserver observer) { + Model model = resolveModel(request); + validateRequestAgainstModel(model, request); + ModelProviderGateway gateway = gatewayRegistry.getGateway(model.getModelProvider().getProviderType()); + gateway.chatStream(model, request, observer); + } + + private Model resolveModel(UnifiedChatRequest request) { + if (request == null || StrUtil.isBlank(request.getModel())) { + throw ModelInvokeException.badRequest("model 不能为空", "model", "model_required"); + } + Model model = modelService.getModelInstanceByInvokeCode(request.getModel()); + if (model == null) { + throw ModelInvokeException.notFound("未找到可调用模型: " + request.getModel()); + } + if (!Boolean.TRUE.equals(model.getPublishEnabled())) { + throw ModelInvokeException.badRequest("当前模型未开启 API 调用发布", "model", "model_not_published"); + } + if (!Model.MODEL_TYPES[0].equals(model.getModelType())) { + throw ModelInvokeException.badRequest("当前模型不是 chatModel,无法通过 chat/completions 调用", "model", "model_type_mismatch"); + } + if (model.getModelProvider() == null || StrUtil.isBlank(model.getModelProvider().getProviderType())) { + throw ModelInvokeException.badRequest("当前模型缺少 providerType 配置", "model", "provider_type_missing"); + } + return model; + } + + private void validateRequestAgainstModel(Model model, UnifiedChatRequest request) { + List messages = request.getMessages(); + if (messages == null || messages.isEmpty()) { + throw ModelInvokeException.badRequest("messages 不能为空", "messages", "messages_required"); + } + if (hasImageInput(messages)) { + if (!Boolean.TRUE.equals(model.getSupportImage())) { + throw ModelInvokeException.badRequest("当前模型不支持图片输入", "messages", "image_not_supported"); + } + if (Boolean.TRUE.equals(model.getSupportImageB64Only()) && hasNonBase64Image(messages)) { + throw ModelInvokeException.badRequest("当前模型仅支持 base64 图片输入", "messages", "image_base64_only"); + } + } + if (request.getTools() != null && !request.getTools().isEmpty() && !Boolean.TRUE.equals(model.getSupportTool())) { + throw ModelInvokeException.badRequest("当前模型不支持 tools 参数", "tools", "tool_not_supported"); + } + if (hasToolMessage(messages) && !Boolean.TRUE.equals(model.getSupportToolMessage())) { + throw ModelInvokeException.badRequest("当前模型不支持 tool 消息透传", "messages", "tool_message_not_supported"); + } + } + + private boolean hasImageInput(List messages) { + return messages.stream() + .map(UnifiedMessage::getContentParts) + .filter(parts -> parts != null && !parts.isEmpty()) + .flatMap(List::stream) + .anyMatch(part -> "image_url".equals(part.getType())); + } + + private boolean hasNonBase64Image(List messages) { + return messages.stream() + .map(UnifiedMessage::getContentParts) + .filter(parts -> parts != null && !parts.isEmpty()) + .flatMap(List::stream) + .filter(part -> "image_url".equals(part.getType())) + .map(UnifiedContentPart::getImageUrl) + .filter(imageUrl -> imageUrl != null && StrUtil.isNotBlank(imageUrl.getUrl())) + .anyMatch(imageUrl -> !StrUtil.startWithIgnoreCase(imageUrl.getUrl(), "data:")); + } + + private boolean hasToolMessage(List messages) { + return messages.stream().anyMatch(message -> + StrUtil.equals(message.getRole(), "tool") + || (message.getToolCalls() != null && !message.getToolCalls().isEmpty()) + || StrUtil.isNotBlank(message.getToolCallId()) + ); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java index b9b1f2a..f07d502 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/ModelService.java @@ -24,4 +24,14 @@ public interface ModelService extends IService { void removeByEntity(Model entity); Model getModelInstance(BigInteger modelId); + + Model getModelInstanceByInvokeCode(String invokeCode); + + void validateForSaveOrUpdate(Model entity, boolean isSave); + + List listInvokeModels(); + + Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled); + + List batchUpdateInvokePublishStatus(List ids, Boolean publishEnabled); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java index 9a102b3..c10b18a 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java @@ -15,6 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import tech.easyflow.ai.entity.Model; import tech.easyflow.ai.entity.ModelProvider; @@ -182,6 +183,125 @@ public class ModelServiceImpl extends ServiceImpl implements throw new BusinessException("模型ID不能为空"); } Model model = modelMapper.selectOneWithRelationsById(modelId); + return fillProviderDefaults(model); + } + + @Override + public Model getModelInstanceByInvokeCode(String invokeCode) { + if (StrUtil.isBlank(invokeCode)) { + throw new BusinessException("invokeCode不能为空"); + } + QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getInvokeCode, invokeCode.trim()); + Model model = modelMapper.selectOneWithRelationsByQuery(queryWrapper); + return fillProviderDefaults(model); + } + + @Override + public void validateForSaveOrUpdate(Model entity, boolean isSave) { + if (entity == null) { + throw new BusinessException("模型配置不能为空"); + } + if (entity.getPublishEnabled() == null) { + entity.setPublishEnabled(Boolean.FALSE); + } + + String originalInvokeCode = StrUtil.trim(entity.getInvokeCode()); + String invokeCode = originalInvokeCode; + boolean autoGeneratedInvokeCode = StrUtil.isBlank(invokeCode); + if (autoGeneratedInvokeCode) { + invokeCode = buildDefaultInvokeCode(entity.getModelName()); + } + + if (Boolean.TRUE.equals(entity.getPublishEnabled())) { + if (StrUtil.isBlank(invokeCode)) { + throw new BusinessException("开启 API 调用前,请先配置 invokeCode"); + } + if (!Model.MODEL_TYPES[0].equals(entity.getModelType())) { + throw new BusinessException("只有聊天模型支持开启 API 调用"); + } + } + + if (StrUtil.isBlank(invokeCode)) { + entity.setInvokeCode(null); + return; + } + + QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getInvokeCode, invokeCode); + if (!isSave && entity.getId() != null) { + queryWrapper.ne(Model::getId, entity.getId()); + } + boolean duplicated = modelMapper.selectCountByQuery(queryWrapper) > 0; + if (duplicated && autoGeneratedInvokeCode && !Boolean.TRUE.equals(entity.getPublishEnabled())) { + entity.setInvokeCode(null); + return; + } + if (duplicated) { + throw new BusinessException("invokeCode 已存在,请更换后重试"); + } + entity.setInvokeCode(invokeCode); + } + + @Override + public List listInvokeModels() { + QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getModelType, Model.MODEL_TYPES[0]); + return modelMapper.selectListWithRelationsByQuery(queryWrapper).stream() + .map(this::fillProviderDefaults) + .collect(Collectors.toList()); + } + + @Override + public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) { + Model existing = getModelInstance(id); + if (existing == null) { + throw new BusinessException("模型不存在"); + } + if (!Model.MODEL_TYPES[0].equals(existing.getModelType())) { + throw new BusinessException("只有聊天模型支持统一网关配置"); + } + + existing.setInvokeCode(invokeCode); + existing.setPublishEnabled(Boolean.TRUE.equals(publishEnabled)); + validateForSaveOrUpdate(existing, false); + modelMapper.update(existing); + return getModelInstance(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List batchUpdateInvokePublishStatus(List ids, Boolean publishEnabled) { + if (CollectionUtils.isEmpty(ids)) { + throw new BusinessException("请选择要操作的模型"); + } + + List uniqueIds = new ArrayList<>(new LinkedHashSet<>(ids)); + List updatedModels = new ArrayList<>(uniqueIds.size()); + for (BigInteger id : uniqueIds) { + updatedModels.add(updateInvokeConfig(id, null, publishEnabled)); + } + return updatedModels; + } + + private String buildDefaultInvokeCode(String modelName) { + if (StrUtil.isBlank(modelName)) { + return null; + } + String normalized = modelName.trim() + .replaceAll("[^A-Za-z0-9._:-]+", "-") + .replaceAll("-{2,}", "-") + .replaceFirst("^[^A-Za-z0-9]+", ""); + if (StrUtil.isBlank(normalized)) { + return null; + } + if (normalized.length() > 128) { + normalized = normalized.substring(0, 128); + } + if (normalized.length() == 1) { + normalized = normalized + "-model"; + } + return normalized; + } + + private Model fillProviderDefaults(Model model) { if (model == null) { return null; } diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapperTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapperTest.java new file mode 100644 index 0000000..c6c8594 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/invoke/mapper/OpenAiProtocolMapperTest.java @@ -0,0 +1,192 @@ +package tech.easyflow.ai.invoke.mapper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; +import tech.easyflow.ai.invoke.exception.ModelInvokeException; +import tech.easyflow.ai.invoke.model.UnifiedChatRequest; +import tech.easyflow.ai.invoke.model.UnifiedChatResponse; +import tech.easyflow.ai.invoke.model.UnifiedChoice; +import tech.easyflow.ai.invoke.model.UnifiedContentPart; +import tech.easyflow.ai.invoke.model.UnifiedImageUrl; +import tech.easyflow.ai.invoke.model.UnifiedMessage; +import tech.easyflow.ai.invoke.model.UnifiedResponseFormat; +import tech.easyflow.ai.invoke.model.UnifiedTool; +import tech.easyflow.ai.invoke.model.UnifiedToolCall; +import tech.easyflow.ai.invoke.model.UnifiedToolCallFunction; +import tech.easyflow.ai.invoke.model.UnifiedToolFunction; +import tech.easyflow.ai.invoke.model.UnifiedUsage; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest; +import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse; + +import java.util.Collections; +import java.util.List; + +public class OpenAiProtocolMapperTest { + + private final OpenAiProtocolMapper mapper = new OpenAiProtocolMapper(new ObjectMapper()); + + @Test + public void shouldParseTextAndImageRequest() { + String rawBody = """ + { + "model": "gpt-4-1-prod", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "帮我看下这张图" + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,AAAA", + "detail": "high" + } + } + ] + } + ], + "stream": false, + "temperature": 0.2, + "top_p": 0.8, + "max_tokens": 512, + "seed": 7, + "tools": [ + { + "type": "function", + "function": { + "name": "query_weather", + "description": "query weather", + "parameters": { + "type": "object" + } + } + } + ], + "tool_choice": "auto", + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "weather_schema" + } + } + } + """; + + OpenAiChatCompletionRequest request = mapper.readRequest(rawBody); + UnifiedChatRequest unifiedRequest = mapper.toUnifiedRequest(request); + + Assert.assertEquals("gpt-4-1-prod", unifiedRequest.getModel()); + Assert.assertEquals(Long.valueOf(7), unifiedRequest.getSeed()); + Assert.assertEquals(Integer.valueOf(512), unifiedRequest.getMaxTokens()); + Assert.assertNotNull(unifiedRequest.getTools()); + Assert.assertEquals(1, unifiedRequest.getTools().size()); + Assert.assertEquals("query_weather", unifiedRequest.getTools().get(0).getFunction().getName()); + Assert.assertEquals("json_schema", unifiedRequest.getResponseFormat().getType()); + Assert.assertEquals("weather_schema", unifiedRequest.getResponseFormat().getJsonSchema().get("name").asText()); + + UnifiedMessage message = unifiedRequest.getMessages().get(0); + Assert.assertEquals("user", message.getRole()); + Assert.assertNotNull(message.getContentParts()); + Assert.assertEquals(2, message.getContentParts().size()); + UnifiedContentPart imagePart = message.getContentParts().get(1); + Assert.assertEquals("image_url", imagePart.getType()); + Assert.assertEquals("data:image/png;base64,AAAA", imagePart.getImageUrl().getUrl()); + Assert.assertEquals("high", imagePart.getImageUrl().getDetail()); + } + + @Test + public void shouldRejectUnsupportedRootField() { + String rawBody = """ + { + "model": "gpt-4-1-prod", + "messages": [ + { + "role": "user", + "content": "hello" + } + ], + "n": 2 + } + """; + + ModelInvokeException exception = Assert.assertThrows( + ModelInvokeException.class, + () -> mapper.readRequest(rawBody) + ); + + Assert.assertEquals(400, exception.getStatus()); + Assert.assertEquals("unsupported_field", exception.getCode()); + } + + @Test + public void shouldAllowMissingOptionalFields() { + String rawBody = """ + { + "model": "deepseek-chat", + "messages": [ + { + "role": "user", + "content": "你好,介绍一下你自己。" + } + ] + } + """; + + OpenAiChatCompletionRequest request = mapper.readRequest(rawBody); + UnifiedChatRequest unifiedRequest = mapper.toUnifiedRequest(request); + + Assert.assertEquals("deepseek-chat", unifiedRequest.getModel()); + Assert.assertNotNull(unifiedRequest.getMessages()); + Assert.assertEquals(1, unifiedRequest.getMessages().size()); + Assert.assertNull(unifiedRequest.getTools()); + Assert.assertNull(unifiedRequest.getToolChoice()); + Assert.assertNull(unifiedRequest.getResponseFormat()); + } + + @Test + public void shouldMapToolCallsAndUsageInResponse() { + UnifiedChatResponse response = new UnifiedChatResponse(); + response.setId("chatcmpl-1"); + response.setObject("chat.completion"); + response.setCreated(123L); + response.setModel("gpt-4-1-prod"); + + UnifiedToolCallFunction toolCallFunction = new UnifiedToolCallFunction(); + toolCallFunction.setName("query_weather"); + toolCallFunction.setArguments("{\"city\":\"shanghai\"}"); + + UnifiedToolCall toolCall = new UnifiedToolCall(); + toolCall.setId("call_1"); + toolCall.setType("function"); + toolCall.setFunction(toolCallFunction); + + UnifiedMessage message = new UnifiedMessage(); + message.setRole("assistant"); + message.setContent(null); + message.setToolCalls(Collections.singletonList(toolCall)); + + UnifiedChoice choice = new UnifiedChoice(); + choice.setIndex(0); + choice.setMessage(message); + choice.setFinishReason("tool_calls"); + + UnifiedUsage usage = new UnifiedUsage(); + usage.setPromptTokens(12); + usage.setCompletionTokens(34); + usage.setTotalTokens(46); + + response.setChoices(List.of(choice)); + response.setUsage(usage); + + OpenAiChatCompletionResponse openAiResponse = mapper.toOpenAiResponse(response); + + Assert.assertEquals("chatcmpl-1", openAiResponse.getId()); + Assert.assertEquals("tool_calls", openAiResponse.getChoices().get(0).getFinishReason()); + Assert.assertEquals("query_weather", openAiResponse.getChoices().get(0).getMessage().getToolCalls().get(0).getFunction().getName()); + Assert.assertEquals(Integer.valueOf(46), openAiResponse.getUsage().getTotalTokens()); + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyServiceImpl.java index 56334df..1de8c6d 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysApiKeyServiceImpl.java @@ -14,6 +14,7 @@ import tech.easyflow.system.service.SysApiKeyService; import javax.annotation.Resource; import java.math.BigInteger; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -34,21 +35,36 @@ public class SysApiKeyServiceImpl extends ServiceImpl candidateRequestUris = getCandidateRequestUris(requestURI); QueryWrapper w = QueryWrapper.create(); - w.eq(SysApiKeyResource::getRequestInterface, requestURI); - SysApiKeyResource resource = resourceService.getOne(w); - if (resource == null) { + w.in(SysApiKeyResource::getRequestInterface, candidateRequestUris); + List resources = resourceService.list(w); + if (resources == null || resources.isEmpty()) { throw new BusinessException("该接口不存在"); } + List resourceIds = resources.stream() + .map(SysApiKeyResource::getId) + .toList(); QueryWrapper wm = QueryWrapper.create(); wm.eq(SysApiKeyResourceMapping::getApiKeyId, sysApiKey.getId()); - wm.eq(SysApiKeyResourceMapping::getApiKeyResourceId, resource.getId()); + wm.in(SysApiKeyResourceMapping::getApiKeyResourceId, resourceIds); long count = mappingService.count(wm); if (count == 0) { throw new BusinessException("该apiKey无权限访问该接口"); } } + private List getCandidateRequestUris(String requestURI) { + List uris = new ArrayList<>(); + uris.add(requestURI); + if ("/v1/chat/completions".equals(requestURI)) { + uris.add("/public-api/openai/v1/chat/completions"); + } else if ("/public-api/openai/v1/chat/completions".equals(requestURI)) { + uris.add("/v1/chat/completions"); + } + return uris; + } + @Override public SysApiKey getSysApiKey(String apiKey) { QueryWrapper w = QueryWrapper.create(); diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V3__easyflow_seed.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V3__easyflow_seed.sql index 0e2d5cf..87ad370 100644 --- a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V3__easyflow_seed.sql +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V3__easyflow_seed.sql @@ -186,34 +186,6 @@ INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000003, 'Kimi', 'kimi', '', '', 'https://api.moonshot.cn', '/v1/chat/completions', '', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000004, '自部署', 'self-hosted', '', '', 'http://127.0.0.1:8000', '/v1/chat/completions', '/v1/embeddings', '/v1/score', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); --- ---------------------------- --- Records of tb_model --- ---------------------------- -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010001, 1, 1000000, 359111120310632448, 'DeepSeek-V3', NULL, '通用对话与代码任务表现均衡。', NULL, NULL, 'deepseek-chat', NULL, NULL, '{"llmEndpoint":"https://api.deepseek.com","chatPath":"/chat/completions","embedPath":"","rerankPath":""}', 'DeepSeek', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010002, 1, 1000000, 359111120310632448, 'DeepSeek-R1', NULL, '复杂推理与长链路分析场景。', NULL, NULL, 'deepseek-reasoner', NULL, NULL, '{"llmEndpoint":"https://api.deepseek.com","chatPath":"/chat/completions","embedPath":"","rerankPath":""}', 'DeepSeek', 'chatModel', 1, 1, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010003, 1, 1000000, 359111228158771200, 'o4-mini', NULL, '轻量推理与通用生产任务模型。', NULL, NULL, 'o4-mini', NULL, NULL, '{"llmEndpoint":"https://api.openai.com","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":""}', 'OpenAI', 'chatModel', 1, 1, 1, 1, 0, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010004, 1, 1000000, 359111228158771200, 'GPT-4.1', NULL, '复杂任务与多模态理解旗舰模型。', NULL, NULL, 'gpt-4.1', NULL, NULL, '{"llmEndpoint":"https://api.openai.com","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":""}', 'OpenAI', 'chatModel', 1, 1, 1, 1, 0, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010005, 1, 1000000, 359111228158771200, 'text-embedding-3-large', NULL, '高质量文本向量模型。', NULL, NULL, 'text-embedding-3-large', NULL, NULL, '{"llmEndpoint":"https://api.openai.com","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":""}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010006, 1, 1000000, 359111448204541952, '通义千问 Plus', NULL, '适合通用问答与工具调用。', NULL, NULL, 'qwen-plus', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Qwen', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010007, 1, 1000000, 359111448204541952, '通义千问 Max', NULL, '综合能力更强的主力模型。', NULL, NULL, 'qwen-max', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Qwen', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010008, 1, 1000000, 359111448204541952, 'text-embedding-v4', NULL, '阿里百炼默认向量模型。', NULL, NULL, 'text-embedding-v4', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010009, 1, 1000000, 359111448204541952, 'gte-rerank-v2', NULL, '阿里百炼默认重排模型。', NULL, NULL, 'gte-rerank-v2', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Rerank', 'rerankModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010010, 1, 1000000, 366100000000000001, 'GLM-4.5', NULL, '中文与推理能力表现均衡。', NULL, NULL, 'glm-4.5', NULL, NULL, '{"llmEndpoint":"https://open.bigmodel.cn","chatPath":"/api/paas/v4/chat/completions","embedPath":"/api/paas/v4/embeddings","rerankPath":""}', 'GLM', 'chatModel', 1, 1, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010011, 1, 1000000, 366100000000000001, 'GLM-4.5-Air', NULL, '低延迟通用模型。', NULL, NULL, 'glm-4.5-air', NULL, NULL, '{"llmEndpoint":"https://open.bigmodel.cn","chatPath":"/api/paas/v4/chat/completions","embedPath":"/api/paas/v4/embeddings","rerankPath":""}', 'GLM', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010012, 1, 1000000, 366100000000000001, 'Embedding-3', NULL, '知识库与检索常用向量模型。', NULL, NULL, 'embedding-3', NULL, NULL, '{"llmEndpoint":"https://open.bigmodel.cn","chatPath":"/api/paas/v4/chat/completions","embedPath":"/api/paas/v4/embeddings","rerankPath":""}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010013, 1, 1000000, 366100000000000002, 'MiniMax-M2.5', NULL, '长上下文与复杂推理场景。', NULL, NULL, 'MiniMax-M2.5', NULL, NULL, '{"llmEndpoint":"https://api.minimax.io","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'MiniMax', 'chatModel', 1, 1, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010014, 1, 1000000, 366100000000000002, 'MiniMax-M2.5-highspeed', NULL, '更高吞吐的快速对话模型。', NULL, NULL, 'MiniMax-M2.5-highspeed', NULL, NULL, '{"llmEndpoint":"https://api.minimax.io","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'MiniMax', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010015, 1, 1000000, 366100000000000003, 'moonshot-v1-8k', NULL, '低延迟通用对话模型。', NULL, NULL, 'moonshot-v1-8k', NULL, NULL, '{"llmEndpoint":"https://api.moonshot.cn","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'Kimi', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010016, 1, 1000000, 366100000000000003, 'moonshot-v1-128k', NULL, '长文档理解与大上下文任务。', NULL, NULL, 'moonshot-v1-128k', NULL, NULL, '{"llmEndpoint":"https://api.moonshot.cn","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'Kimi', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010017, 1, 1000000, 359110667376132096, 'DeepSeek-V3', NULL, '统一平台接入的开源对话模型。', NULL, NULL, 'deepseek-ai/DeepSeek-V3', NULL, NULL, '{"llmEndpoint":"https://api.siliconflow.cn","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/rerank"}', 'DeepSeek', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010018, 1, 1000000, 359110667376132096, 'Kimi-K2-Instruct', NULL, '代码与 Agent 工作流场景常用模型。', NULL, NULL, 'moonshotai/Kimi-K2-Instruct', NULL, NULL, '{"llmEndpoint":"https://api.siliconflow.cn","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/rerank"}', 'Kimi', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010019, 1, 1000000, 359110667376132096, 'BAAI/bge-m3', NULL, '多语言检索与向量召回模型。', NULL, NULL, 'BAAI/bge-m3', NULL, NULL, '{"llmEndpoint":"https://api.siliconflow.cn","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/rerank"}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010020, 1, 1000000, 359110690079899648, 'qwen3:8b', NULL, '本地开发常用的通用对话模型。', NULL, NULL, 'qwen3:8b', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:11434","chatPath":"/v1/chat/completions","embedPath":"/api/embed","rerankPath":""}', 'Ollama', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010021, 1, 1000000, 359110690079899648, 'bge-m3', NULL, '本地向量化与检索验证。', NULL, NULL, 'bge-m3', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:11434","chatPath":"/v1/chat/completions","embedPath":"/api/embed","rerankPath":""}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010022, 1, 1000000, 366100000000000004, 'Qwen/Qwen3-32B', NULL, '适合自部署对话与工具调用。', NULL, NULL, 'Qwen/Qwen3-32B', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:8000","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/score"}', 'Qwen', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010023, 1, 1000000, 366100000000000004, 'BAAI/bge-m3', NULL, '自部署检索和知识库向量化。', NULL, NULL, 'BAAI/bge-m3', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:8000","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/score"}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); -INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010024, 1, 1000000, 366100000000000004, 'jinaai/jina-reranker-m0', NULL, '自部署召回重排模型。', NULL, NULL, 'jinaai/jina-reranker-m0', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:8000","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/score"}', 'Rerank', 'rerankModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0); - -- ---------------------------- -- Records of tb_sys_option -- ---------------------------- diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V7__model_invoke_gateway.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V7__model_invoke_gateway.sql new file mode 100644 index 0000000..848b838 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/V7__model_invoke_gateway.sql @@ -0,0 +1,18 @@ +SET NAMES utf8mb4; + +ALTER TABLE `tb_model` + ADD COLUMN `invoke_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '统一模型调用对外标识' AFTER `support_tool_message`, + ADD COLUMN `publish_enabled` tinyint NULL DEFAULT 0 COMMENT '是否开启统一模型调用发布' AFTER `invoke_code`; + +CREATE UNIQUE INDEX `uni_model_invoke_code` ON `tb_model` (`invoke_code`); + +INSERT INTO `tb_sys_api_key_resource` (`id`, `request_interface`, `title`) +SELECT 366700000000000001, + '/v1/chat/completions', + '统一模型调用(OpenAI Chat Completions)' +FROM DUAL +WHERE NOT EXISTS( + SELECT 1 + FROM `tb_sys_api_key_resource` + WHERE `request_interface` = '/v1/chat/completions' + ); diff --git a/easyflow-ui-admin/app/public/model-providers/aliyun.png b/easyflow-ui-admin/app/public/model-providers/aliyun.png deleted file mode 100644 index 29fbfa6813e74ecd2aab527b125f57af8dacadfe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7399 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 F{tpizLT&&6 diff --git a/easyflow-ui-admin/app/public/model-providers/kimi.png b/easyflow-ui-admin/app/public/model-providers/kimi.png deleted file mode 100644 index 294e9efcb53acb95bacfe32a38fca1521a18987d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7295 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 Z{68os#UUsSnSuZS002ovPDHLkV1i+-=Rp7f diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/bailian.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/bailian.svg new file mode 100644 index 0000000..9acccd0 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/bailian.svg @@ -0,0 +1 @@ +BaiLian \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/claude.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/claude.svg new file mode 100644 index 0000000..62dc0db --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/deepseek.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/deepseek.svg new file mode 100644 index 0000000..3fc2302 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/doubao.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/doubao.svg new file mode 100644 index 0000000..ec0f976 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/doubao.svg @@ -0,0 +1,2 @@ +429: Too Many Requests +For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service). \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/gemini.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/gemini.svg new file mode 100644 index 0000000..ec0f976 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/gemini.svg @@ -0,0 +1,2 @@ +429: Too Many Requests +For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service). \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/kimi.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/kimi.svg new file mode 100644 index 0000000..fb56ac1 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/kimi.svg @@ -0,0 +1 @@ +MoonshotAI \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/minimax.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/minimax.svg new file mode 100644 index 0000000..2a60bd4 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/minimax.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/ollama.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/ollama.svg new file mode 100644 index 0000000..cc887e3 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/ollama.svg @@ -0,0 +1 @@ +Ollama \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/openai.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/openai.svg new file mode 100644 index 0000000..78caf4f --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/openai.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/qwen.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/qwen.svg new file mode 100644 index 0000000..33b3f64 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/qwen.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/siliconcloud.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/siliconcloud.svg new file mode 100644 index 0000000..6b5f6d8 --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/siliconcloud.svg @@ -0,0 +1 @@ +SiliconCloud \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/vllm.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/vllm.svg new file mode 100644 index 0000000..54acc3d --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/vllm.svg @@ -0,0 +1 @@ +vLLM \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/lobehub/zhipu.svg b/easyflow-ui-admin/app/public/model-providers/lobehub/zhipu.svg new file mode 100644 index 0000000..0c6e61c --- /dev/null +++ b/easyflow-ui-admin/app/public/model-providers/lobehub/zhipu.svg @@ -0,0 +1 @@ +Zhipu \ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/minimax.png b/easyflow-ui-admin/app/public/model-providers/minimax.png deleted file mode 100644 index a79385908ea557fa9d6bb0ee65e327777b5fcc98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1877 zcmV-b2demqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$_zez+vR9FdpS6yrsMHK#Kc5i=y zA|OA8f&m2!429|2sCG3m1m(oc|k2-?ZF@!(1z3z8Bz3m8%E0~aKMPKl9(>Xmerw&zIXBhK^) z$xqosLok0XdlgZXEFqIO3H%I=+7z?}sFtvXp_2$NWi3f zO^i`%t(k)~dWG)KF&S-|m+Zj>0W?Q|td6wo)zkx1nztM5s0nI$8&e@lzq8m&u&x*> z9;3l->a20W(!-TU(iaj|Dafd#Em(=8jEpJELO;Ak5A1?*r-*0zDW4}g*W@KwY<}wc z6fbTo5qfz6s3Hwb3-wb8yJ8pr-CfbaDUrZ%EZb7mz_VZ>8JAbb0+_S^PYkN(4WfHRkpTLcb zAllex+IBq+)76Re^l3DX9t9fJ0^ue-1GmsJVUmSrlFgGYTv-a@jcZNRw-VKX&tTfy zQF&xDPep|I@$GQKqo{p)4EBL7Fe{e(>_~b7UU+*%&7C#5Hn(d^m|t`6q02e18*X9> z$xpwzbaWhMK@U)gVdi!t`Qrl8v*&=>S(uIvZXbcV`_TQ~t`hv=M-~Y+LFp3DZ$AabGVD6Qt*d;bs~%$!Pjc zQ#7wBiDYtJ-LR`x0`==GxfiJPlkqSH%v~vLscS0MbiME}+PC#7)!OmXs2=>Xu#mN* zubkWf5#DMfY`VJ<^*;~0dbLy;xZ}s*P7YELEctm6CSu#v$lNaOLHpKqnBG5#>mMFR z+x88xx6Prkw+@*v`-vuCImhqCqD8Rl)+0GMfOO~t%>7$|_71Vqbr)M%0;wFHQW>F* zDoT^#3D|k<>~p~Lt<76&FzJS%qRUkJb~a+_>u{43!1x$X3#)KU+(vH33}nYZ7Tr`L zbnUzs@m-6#R2i&p{Q#@s^&yhh;G!t+v)oIK^HI)NBzX;xl$y>qbnM)O`0gcS={HNs zP4LUDJn=$)Qz>Uz2Fm(kTATt3U5&eZwt;xfQcWG05m%RdaS2*mmboICJ|pEgg$wf% zh_x7Giz=pEgqb?3$9_cngX=KoRC%s3#lNCBvC#_tX|$seOa-R zi&uC@;cX|<(^7_%=zh$yR~sDC>scmI5XBECMlzReV)y&6NN11 z&OP82cL{;ZVYxSEd0C4QufNkoYkGm18c#`r%9C4Bd171H6s|gh`r(gVN)qm$NhC)O zIU&_lM?w-ALid1#QW7~6As_>4n2F^47-}caqH_;tb$tfa_gPYFb2{jo8CDPM` zW?jvKTvpsPzxA4K^;0u(mSqq{kY?{3k+b>U67c{PmqnRtyk%2#6}3WnyUer2TXo9! zCJF@_C&dq}i|`F2ze`cQ1)q*0DWn4DN0}^FF)F#4HB%C=v4}{*;NP)P##adWCS3!uKeCo11mk_Kv(Js%!iQ z^$j||$lnmhtURIVI?5o+r(oW^Nu;t?c5KO#)eEPn-(ynyI{W`d2fxLCpg*1epzs&c P00000NkvXXu0mjf;;v%_ diff --git a/easyflow-ui-admin/app/public/model-providers/ollama.png b/easyflow-ui-admin/app/public/model-providers/ollama.png deleted file mode 100644 index 5886fb91f2027bda5e92eaca4c0e4be495f1191e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1797 zcmV+g2m1JlP)^L9hX(gL&^i$>q5W$$TXN zp5Hlh2qc-z+_^LNW5R)I8Lf3fA6+VERFa|`6`zs?m1XhyMdcIa;NW1P{xzu6T`FTL zsjg2-rKfYP+7GtWaT7diymLWN1NwTq9XCm-^whS2ShqRY{VlCg6IHj*e8P)2XT#7Gy+LW1`wZ z=kuFtG9A#4jOqIvAHj{{RrbA__d5tN+3}v4WMduUx=&3+3 zio9U)!iEvWrySAi>uW`Bt+g&_2B~;vK^`9;v)@qn-Q8XG94vrx;j7Xx2Z&%N8(N?P z=RPC7K;=1)eehW@WBiMPA@^JQ0nu~c#lypc3dQFhnAAx6e2`56o#)#U70Aua&4yaT zo})lL(LkteOjwM2AOtu&ToqLIOltrGA zQ{ivkKnG@TN#bh_==b}2+YAPSx&c?poe%aV*%fl3jBtuT0s#c|K#o2h>PSqq z3c_KTEsD^B9;`q|1@Q9nvN~=u?-v6WN1Feu@AvojDv%B|z$>U`q{JP$xvgLeoIH4T zrn@9E5Q%tLYsf(foRS}y0#W{Cl^Y5>4gX*?8f`c#_zpF_xVTWq$H$87a8_`P-`M>% z??GLtvnu|NbcwTDRfxD;p(xJF(SWbOAbn+~&C3evmFaZpYFFg}Wk43XC`SvRYzoNy zpb{~Fbehn?@p!z}P^=}eF4DZR@MLZgrEIT33f3c0x<-%y%!ip~$IJ$!Q*x2=ahRlo zgL})e!x9Kj)mWE;1t3*YW@{NV5TnP!Vf8dSkCDQ)TqK+kW(|TMWt}1IaS`kWnKrnO_91csH?mO?~nt|iVWU?}GT=1K`&UZwm z6_AdXCrLIDbbuw0jtNxN7pbnU5|Ut%;dx+gB1PXhEshwsuRM?oPe>?Cb6ru#vVB41 z`OHNQ3nn&8kP;8^?d9Sp^MxMFS4TR3vC#{;0PZpw!qnI*>!R;jTD-`PM(3FSK^B}5 zmiq$1>6CpSa&KV72T}k?6FY0GG3QzV+i;BMnuI>i`O8WG#a@|H3*XV&(!IQxPW<6@s=*1#@?3SIF4G5eM9=7<5Ib zEFT;a4>Sz|D5cgDuq6yt9jJn}G_3h*?5_6)m3s@Wn6Hgw9E6s00000NkvXXu0mjfOPpQf diff --git a/easyflow-ui-admin/app/public/model-providers/openai.ico b/easyflow-ui-admin/app/public/model-providers/openai.ico deleted file mode 100644 index 6f206de..0000000 --- a/easyflow-ui-admin/app/public/model-providers/openai.ico +++ /dev/null @@ -1 +0,0 @@ -Just a moment...
\ No newline at end of file diff --git a/easyflow-ui-admin/app/public/model-providers/openai.svg b/easyflow-ui-admin/app/public/model-providers/openai.svg deleted file mode 100644 index 005b2e4..0000000 --- a/easyflow-ui-admin/app/public/model-providers/openai.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/easyflow-ui-admin/app/public/model-providers/self-hosted.svg b/easyflow-ui-admin/app/public/model-providers/self-hosted.svg deleted file mode 100644 index acdd3f1..0000000 --- a/easyflow-ui-admin/app/public/model-providers/self-hosted.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/easyflow-ui-admin/app/public/model-providers/siliconflow.png b/easyflow-ui-admin/app/public/model-providers/siliconflow.png deleted file mode 100644 index fda2dc6717ab0021d1d82031b5979f357e1ef206..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 583 zcmV-N0=WH&P)Px%0ZBwbRCwC$*1KwyQ51&Z_uG5VkkJuQ5yeww6-z-|3vXc%tgLLsTEy`Z#0#*n z5k&9?1RL$_G*|>Y;E6yKHOz#W|6iX)qC|$|H)9b3^W4B*>)p*_W@cumsgGfL3E&o( ze{>tv2Ls;K1riX;r&s1vJKu27CPj!OHete$S~72A-01^9?k=LjG%k>UC`;`t-B$bZ zU`!=ZXkc*3Xw{=0>dPM!xYSW;Q7QCl%O7|qK=Uf zK?7OUTC!$wNXfh0k5f%Qs31i;Z1%9k#U&OF3GObifYCju~l zY49htFu=o)H+*jfy# z)e^)jfLDk2m?`M`3A4e$x;^YV5j3ccS`ni=Z}u($=Kopj02#A^*gnthx|x|>?H2)m Vztlt0t002ovPDHLkV1l5f0;~W4 diff --git a/easyflow-ui-admin/app/public/model-providers/zhipu.png b/easyflow-ui-admin/app/public/model-providers/zhipu.png deleted file mode 100644 index 8d3640d03de46b12d1bd739ed55dba2a1bc1e598..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2747 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 diff --git a/easyflow-ui-admin/app/src/api/ai/llm.ts b/easyflow-ui-admin/app/src/api/ai/llm.ts index d54c912..9067533 100644 --- a/easyflow-ui-admin/app/src/api/ai/llm.ts +++ b/easyflow-ui-admin/app/src/api/ai/llm.ts @@ -1,4 +1,4 @@ -import {api} from '#/api/request.js'; +import { api } from '#/api/request.js'; // 获取LLM供应商 export async function getLlmProviderList() { @@ -14,6 +14,10 @@ export async function getModelList(params: ModelListQuery = {}) { return api.get('/api/v1/model/list', { params }); } +export async function getInvokeModelList() { + return api.get('/api/v1/model/invokeList'); +} + // 保存LLM export async function saveLlm(data: string) { return api.post('/api/v1/model/save', data); @@ -33,6 +37,27 @@ export async function verifyModelConfig(id: string) { return api.get('/api/v1/model/verifyLlmConfig', { params: { id } }); } +export interface ModelInvokeConfigPayload { + id: string; + invokeCode?: string; + publishEnabled: boolean; +} + +export async function updateModelInvokeConfig(data: ModelInvokeConfigPayload) { + return api.post('/api/v1/model/updateInvokeConfig', data); +} + +export interface BatchModelInvokePublishPayload { + ids: string[]; + publishEnabled: boolean; +} + +export async function batchUpdateInvokePublishStatus( + data: BatchModelInvokePublishPayload, +) { + return api.post('/api/v1/model/batchUpdateInvokePublishStatus', data); +} + // 一键添加LLM export async function quickAddLlm(data: any) { return api.post(`/api/v1/model/quickAdd`, data); @@ -40,7 +65,9 @@ export async function quickAddLlm(data: any) { export interface llmType { id: string; + providerId?: string; title: string; + modelName?: string; modelProvider: { icon: string; providerName: string; @@ -51,6 +78,12 @@ export interface llmType { description: string; modelType: string; groupName: string; + invokeCode?: string; + publishEnabled?: boolean; + supportTool?: boolean; + supportImage?: boolean; + supportImageB64Only?: boolean; + supportToolMessage?: boolean; added: boolean; aiLlmProvider: any; } diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/llm.json b/easyflow-ui-admin/app/src/locales/langs/en-US/llm.json index 659534f..dc4f415 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/llm.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/llm.json @@ -1,45 +1,43 @@ { - "filed": - { - "id": "Id", - "deptId": "DeptId", - "tenantId": "TenantId", - "title": "Title", - "brand": "Brand", - "icon": "Icon", - "description": "Description", - "supportChat": "SupportChat", - "supportFunctionCalling": "SupportFunctionCalling", - "supportEmbed": "SupportEmbed", - "supportReranker": "SupportReranker", - "supportTextToImage": "SupportTextToImage", - "supportImageToImage": "SupportImageToImage", - "supportTextToAudio": "SupportTextToAudio", - "supportAudioToAudio": "SupportAudioToAudio", - "supportTextToVideo": "SupportTextToVideo", - "supportImageToVideo": "SupportImageToVideo", - "multimodal": "multimodal", - "llmEndpoint": "LlmEndpoint", - "chatPath": "ChatPath", - "embedPath": "embedPath", - "llmModel": "LlmModel", - "llmApiKey": "apiKey", - "llmExtraConfig": "LlmExtraConfig", - "options": "Options", - "ability": "Ability" - } - , + "filed": { + "id": "Id", + "deptId": "DeptId", + "tenantId": "TenantId", + "title": "Title", + "brand": "Brand", + "icon": "Icon", + "description": "Description", + "supportChat": "SupportChat", + "supportFunctionCalling": "SupportFunctionCalling", + "supportEmbed": "SupportEmbed", + "supportReranker": "SupportReranker", + "supportTextToImage": "SupportTextToImage", + "supportImageToImage": "SupportImageToImage", + "supportTextToAudio": "SupportTextToAudio", + "supportAudioToAudio": "SupportAudioToAudio", + "supportTextToVideo": "SupportTextToVideo", + "supportImageToVideo": "SupportImageToVideo", + "multimodal": "multimodal", + "llmEndpoint": "LlmEndpoint", + "chatPath": "ChatPath", + "embedPath": "embedPath", + "llmModel": "Model ID", + "llmApiKey": "apiKey", + "llmExtraConfig": "LlmExtraConfig", + "options": "Options", + "ability": "Ability" + }, "llmModal": { "TitleRequired": "Please enter the name", "BrandRequired": "Please enter the brand", - "ModelRequired": "Please enter the model", + "ModelRequired": "Please enter the model ID", "ApiKeyRequired": "Please enter the apiKey", "QuickAddLlm": "One-click addition of large models" }, "placeholder": { "title": "Please enter the title", "brand": "Please enter the brand", - "llmModel": "Please enter the llmModel", + "llmModel": "Please enter the model ID", "description": "Please enter the description" }, "actions": { @@ -50,7 +48,7 @@ }, "addProvider": "Provider list", "modelType": "ModelType", - "llmModel": "ModelName", + "llmModel": "Model ID", "title": "name", "groupName": "GroupName", "provider": "供应商", @@ -62,7 +60,7 @@ }, "all": "All", "verifyLlmTitle": "Verify Large Model", - "searchTextPlaceholder": "Search for model name or name", + "searchTextPlaceholder": "Search for model ID or name", "testSuccess": "TestSuccess", "modelAbility": { "supportThinking": "Thinking", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json b/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json index ebf6ea6..14d7b41 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/settingsConfig.json @@ -2,7 +2,7 @@ "title": "Large Model Configuration", "modelOfChat": "Chat Model Provider", "dialogModel": "Chat Model Settings", - "modelName": "Model Name", + "modelName": "Model ID", "basic": "BasicInformation", "updatePwd": "UpdatePassword", "systemAIFunctionSettings": "System AI Function Settings", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/llm.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/llm.json index 6f02d5e..97b4878 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/llm.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/llm.json @@ -1,9 +1,8 @@ { - "filed": - { + "filed": { "title": "标题", "brand": "供应商", - "llmModel": "大模型名称", + "llmModel": "模型 ID", "icon": "ICON", "description": "描述", "supportChat": "对话模型", @@ -24,19 +23,18 @@ "llmExtraConfig": "大模型其他属性配置", "options": "其他配置内容", "ability": "能力" - } -, + }, "llmModal": { "TitleRequired": "请输入名称", "BrandRequired": "请选择供应商", - "ModelRequired": "请输入大模型", + "ModelRequired": "请输入模型 ID", "ApiKeyRequired": "请输入apiKey", "QuickAddLlm": "一键添加大模型" }, "placeholder": { "title": "请输入名称", "brand": "请选择品牌", - "llmModel": "请输入大模型名称", + "llmModel": "请输入模型 ID", "description": "请输入描述" }, "actions": { @@ -47,7 +45,7 @@ }, "addProvider": "供应商列表", "modelType": "模型类型", - "llmModel": "模型名称", + "llmModel": "模型 ID", "title": "名称", "groupName": "分组名称", "provider": "供应商", @@ -60,7 +58,7 @@ "all": "全部", "verifyLlmTitle": "请选择要检测的模型", "testSuccess": "检测成功", - "searchTextPlaceholder": "搜索模型名称或名称", + "searchTextPlaceholder": "搜索模型 ID 或名称", "modelAbility": { "supportThinking": "推理", "supportTool": "工具", @@ -73,5 +71,4 @@ }, "requestPath": "请求路径", "modelToBeTested": "待检测模型" - } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json index a75075b..ee4e8dc 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/settingsConfig.json @@ -2,7 +2,7 @@ "title": "大模型配置", "modelOfChat": "对话模型供应商", "dialogModel": "对话模型配置", - "modelName": "模型名称", + "modelName": "模型 ID", "basic": "基本设置", "updatePwd": "修改密码", "systemAIFunctionSettings": "系统 AI 功能设置", diff --git a/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue b/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue index 6c9972f..a679d67 100644 --- a/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue +++ b/easyflow-ui-admin/app/src/views/ai/model/ActiveModelWorkspace.vue @@ -1,11 +1,17 @@