feat: 新增统一模型网关与模型管理工作区

- 新增 OpenAI 兼容统一模型调用链路、模型发布配置与批量发布能力

- 重构模型管理页面入口与统一网关工作区,更新服务商 logo 资源与模型 ID 文案

- 收口全新库初始化脚本,仅保留服务商种子并整理统一网关 migration
This commit is contained in:
2026-03-26 20:48:18 +08:00
parent b777cb3641
commit aaf4c61ff8
80 changed files with 4786 additions and 362 deletions

View File

@@ -41,6 +41,10 @@
<groupId>com.easyagents</groupId>
<artifactId>easy-agents-support</artifactId>
</dependency>
<dependency>
<groupId>com.easyagents</groupId>
<artifactId>easy-agents-spring-boot-starter</artifactId>
</dependency>
<!--使用
enjoy 模板引擎-->
<dependency>

View File

@@ -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<BigInteger> ids;
private Boolean publishEnabled;
public List<BigInteger> getIds() {
return ids;
}
public void setIds(List<BigInteger> ids) {
this.ids = ids;
}
public Boolean getPublishEnabled() {
return publishEnabled;
}
public void setPublishEnabled(Boolean publishEnabled) {
this.publishEnabled = publishEnabled;
}
}
}

View File

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

View File

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

View File

@@ -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<String> ROOT_FIELDS = Set.of(
"model", "messages", "stream", "temperature", "top_p", "max_tokens",
"seed", "tools", "tool_choice", "response_format"
);
private static final Set<String> MESSAGE_FIELDS = Set.of(
"role", "content", "name", "tool_call_id", "tool_calls"
);
private static final Set<String> CONTENT_PART_FIELDS = Set.of("type", "text", "image_url");
private static final Set<String> IMAGE_URL_FIELDS = Set.of("url", "detail");
private static final Set<String> TOOL_FIELDS = Set.of("type", "function");
private static final Set<String> TOOL_FUNCTION_FIELDS = Set.of("name", "description", "parameters");
private static final Set<String> RESPONSE_FORMAT_FIELDS = Set.of("type", "json_schema");
private static final Set<String> 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<OpenAiChatCompletionRequest.Message> buildMessages(JsonNode messagesNode) {
List<OpenAiChatCompletionRequest.Message> 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<String> allowedFields, String path) {
Iterator<String> fieldNames = node.fieldNames();
Set<String> 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<UnifiedMessage> toUnifiedMessages(List<OpenAiChatCompletionRequest.Message> messages) {
if (messages == null) {
return null;
}
List<UnifiedMessage> 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<OpenAiChatCompletionRequest.Message> toOpenAiMessages(List<UnifiedMessage> messages) {
if (messages == null) {
return null;
}
List<OpenAiChatCompletionRequest.Message> 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<UnifiedContentPart> toUnifiedContentParts(List<OpenAiChatCompletionRequest.ContentPart> contentParts) {
if (contentParts == null) {
return null;
}
List<UnifiedContentPart> 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<OpenAiChatCompletionRequest.ContentPart> toOpenAiContentParts(List<UnifiedContentPart> contentParts) {
if (contentParts == null) {
return null;
}
List<OpenAiChatCompletionRequest.ContentPart> 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<UnifiedTool> toUnifiedTools(List<OpenAiChatCompletionRequest.Tool> tools) {
if (tools == null) {
return null;
}
List<UnifiedTool> 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<OpenAiChatCompletionRequest.Tool> toOpenAiTools(List<UnifiedTool> tools) {
if (tools == null) {
return null;
}
List<OpenAiChatCompletionRequest.Tool> 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<UnifiedChoice> toUnifiedChoices(List<OpenAiChatCompletionResponse.Choice> choices, boolean delta) {
if (choices == null) {
return null;
}
List<UnifiedChoice> 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<UnifiedChoice> toUnifiedChunkChoices(List<OpenAiChatCompletionChunkResponse.Choice> choices) {
if (choices == null) {
return null;
}
List<UnifiedChoice> 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<OpenAiChatCompletionResponse.Choice> toOpenAiChoices(List<UnifiedChoice> choices, boolean delta) {
if (choices == null) {
return null;
}
List<OpenAiChatCompletionResponse.Choice> 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<OpenAiChatCompletionChunkResponse.Choice> toOpenAiChunkChoices(List<UnifiedChoice> choices) {
if (choices == null) {
return null;
}
List<OpenAiChatCompletionChunkResponse.Choice> 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<UnifiedToolCall> toUnifiedToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
if (toolCalls == null) {
return null;
}
List<UnifiedToolCall> 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<OpenAiChatCompletionResponse.ToolCall> toOpenAiToolCalls(List<UnifiedToolCall> toolCalls) {
if (toolCalls == null) {
return null;
}
List<OpenAiChatCompletionResponse.ToolCall> 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;
}
}

View File

@@ -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<UnifiedChoice> 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<UnifiedChoice> getChoices() {
return choices;
}
public void setChoices(List<UnifiedChoice> choices) {
this.choices = choices;
}
public UnifiedUsage getUsage() {
return usage;
}
public void setUsage(UnifiedUsage usage) {
this.usage = usage;
}
}

View File

@@ -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<UnifiedMessage> messages;
private Boolean stream;
private Double temperature;
private Double topP;
private Integer maxTokens;
private Long seed;
private List<UnifiedTool> tools;
private JsonNode toolChoice;
private UnifiedResponseFormat responseFormat;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<UnifiedMessage> getMessages() {
return messages;
}
public void setMessages(List<UnifiedMessage> 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<UnifiedTool> getTools() {
return tools;
}
public void setTools(List<UnifiedTool> 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;
}
}

View File

@@ -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<UnifiedChoice> 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<UnifiedChoice> getChoices() {
return choices;
}
public void setChoices(List<UnifiedChoice> choices) {
this.choices = choices;
}
public UnifiedUsage getUsage() {
return usage;
}
public void setUsage(UnifiedUsage usage) {
this.usage = usage;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<UnifiedContentPart> contentParts;
private String name;
private String toolCallId;
private List<UnifiedToolCall> 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<UnifiedContentPart> getContentParts() {
return contentParts;
}
public void setContentParts(List<UnifiedContentPart> 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<UnifiedToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<UnifiedToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Choice> 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<Choice> getChoices() {
return choices;
}
public void setChoices(List<Choice> 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<OpenAiChatCompletionResponse.ToolCall> 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<OpenAiChatCompletionResponse.ToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
}
}

View File

@@ -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<Message> 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<Tool> 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<Message> getMessages() {
return messages;
}
public void setMessages(List<Message> 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<Tool> getTools() {
return tools;
}
public void setTools(List<Tool> 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<ContentPart> 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<OpenAiChatCompletionResponse.ToolCall> 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<ContentPart> 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<OpenAiChatCompletionResponse.ToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<OpenAiChatCompletionResponse.ToolCall> 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;
}
}
}

View File

@@ -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<Choice> 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<Choice> getChoices() {
return choices;
}
public void setChoices(List<Choice> 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<ToolCall> 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<ToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<ToolCall> 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;
}
}
}

View File

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

View File

@@ -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<OpenAiChatCompletionRequest.ContentPart> convertContentParts(JsonNode node) {
return OBJECT_MAPPER.convertValue(node, new TypeReference<>() {
});
}
}

View File

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

View File

@@ -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<ModelProviderGateway> gateways;
public ModelProviderGatewayRegistry(List<ModelProviderGateway> 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"
));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<UnifiedMessage> 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<UnifiedMessage> 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<UnifiedMessage> 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<UnifiedMessage> messages) {
return messages.stream().anyMatch(message ->
StrUtil.equals(message.getRole(), "tool")
|| (message.getToolCalls() != null && !message.getToolCalls().isEmpty())
|| StrUtil.isNotBlank(message.getToolCallId())
);
}
}

View File

@@ -24,4 +24,14 @@ public interface ModelService extends IService<Model> {
void removeByEntity(Model entity);
Model getModelInstance(BigInteger modelId);
Model getModelInstanceByInvokeCode(String invokeCode);
void validateForSaveOrUpdate(Model entity, boolean isSave);
List<Model> listInvokeModels();
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled);
}

View File

@@ -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<ModelMapper, Model> 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<Model> 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<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled) {
if (CollectionUtils.isEmpty(ids)) {
throw new BusinessException("请选择要操作的模型");
}
List<BigInteger> uniqueIds = new ArrayList<>(new LinkedHashSet<>(ids));
List<Model> 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;
}

View File

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

View File

@@ -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<SysApiKeyMapper, SysApiKey
@Override
public void checkApikeyPermission(String apiKey, String requestURI) {
SysApiKey sysApiKey = getSysApiKey(apiKey);
List<String> 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<SysApiKeyResource> resources = resourceService.list(w);
if (resources == null || resources.isEmpty()) {
throw new BusinessException("该接口不存在");
}
List<BigInteger> 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<String> getCandidateRequestUris(String requestURI) {
List<String> 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();