feat: 新增统一模型网关与模型管理工作区
- 新增 OpenAI 兼容统一模型调用链路、模型发布配置与批量发布能力 - 重构模型管理页面入口与统一网关工作区,更新服务商 logo 资源与模型 ID 文案 - 收口全新库初始化脚本,仅保留服务商种子并整理统一网关 migration
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user