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 29fbfa6..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/aliyun.png and /dev/null differ diff --git a/easyflow-ui-admin/app/public/model-providers/deepseek.png b/easyflow-ui-admin/app/public/model-providers/deepseek.png deleted file mode 100644 index 882954c..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/deepseek.png and /dev/null differ 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 294e9ef..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/kimi.png and /dev/null differ 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 a793859..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/minimax.png and /dev/null differ 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 5886fb9..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/ollama.png and /dev/null differ 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 fda2dc6..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/siliconflow.png and /dev/null differ 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 8d3640d..0000000 Binary files a/easyflow-ui-admin/app/public/model-providers/zhipu.png and /dev/null differ 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 @@