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

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

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

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

View File

@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.dto.ModelInvokeConfigDtos;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.ai.entity.table.ModelTableDef;
@@ -69,6 +70,12 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
return Result.ok(modelService.getList(entity));
}
@GetMapping("invokeList")
@SaCheckPermission("/api/v1/model/query")
public Result<List<Model>> invokeList() {
return Result.ok(modelService.listInvokeModels());
}
@PostMapping("/addAiLlm")
@SaCheckPermission("/api/v1/model/save")
public Result<Boolean> addAiLlm(Model entity) {
@@ -92,6 +99,31 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
return Result.ok();
}
@PostMapping("/updateInvokeConfig")
@SaCheckPermission("/api/v1/model/save")
public Result<Model> updateInvokeConfig(@RequestBody ModelInvokeConfigDtos.UpdateRequest request) {
return Result.ok(modelService.updateInvokeConfig(
request.getId(),
request.getInvokeCode(),
request.getPublishEnabled()
));
}
@PostMapping("/batchUpdateInvokePublishStatus")
@SaCheckPermission("/api/v1/model/save")
public Result<List<Model>> batchUpdateInvokePublishStatus(@RequestBody ModelInvokeConfigDtos.BatchPublishRequest request) {
return Result.ok(modelService.batchUpdateInvokePublishStatus(
request.getIds(),
request.getPublishEnabled()
));
}
@Override
protected Result<?> onSaveOrUpdateBefore(Model entity, boolean isSave) {
modelService.validateForSaveOrUpdate(entity, isSave);
return super.onSaveOrUpdateBefore(entity, isSave);
}
@GetMapping("/selectLlmByProviderCategory")
@SaCheckPermission("/api/v1/model/query")
public Result<Map<String, List<Model>>> selectLlmByProviderCategory(Model entity, String sortKey, String sortType) {

View File

@@ -0,0 +1,176 @@
package tech.easyflow.publicapi.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiErrorResponse;
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.service.SysApiKeyService;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.Duration;
@SaIgnore
@RestController
@RequestMapping("/v1")
public class PublicModelChatController {
private static final Logger log = LoggerFactory.getLogger(PublicModelChatController.class);
private static final long SSE_TIMEOUT = Duration.ofMinutes(10).toMillis();
@Resource
private SysApiKeyService sysApiKeyService;
@Resource
private UnifiedModelInvokeService unifiedModelInvokeService;
@Resource
private OpenAiProtocolMapper openAiProtocolMapper;
@Resource
private ObjectMapper objectMapper;
/**
* 统一模型调用OpenAI Chat Completions
*/
@PostMapping(
value = "/chat/completions",
produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.TEXT_EVENT_STREAM_VALUE
}
)
public Object chatCompletions(@RequestBody String rawBody, HttpServletRequest request) {
try {
String apiKey = resolveApiKey(request.getHeader(HttpHeaders.AUTHORIZATION));
sysApiKeyService.checkApikeyPermission(apiKey, request.getRequestURI());
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.readRequest(rawBody);
UnifiedChatRequest unifiedRequest = openAiProtocolMapper.toUnifiedRequest(openAiRequest);
if (Boolean.TRUE.equals(unifiedRequest.getStream())) {
return createStreamEmitter(unifiedRequest);
}
UnifiedChatResponse response = unifiedModelInvokeService.chat(unifiedRequest);
return buildJsonResponse(openAiProtocolMapper.toOpenAiResponse(response));
} catch (ModelInvokeException e) {
return buildErrorResponse(e);
} catch (BusinessException e) {
return buildErrorResponse(mapBusinessException(e));
} catch (Exception e) {
log.error("chatCompletions unexpected error: {}", e.getMessage(), e);
return buildErrorResponse(new ModelInvokeException(
500,
"统一模型调用失败: " + e.getMessage(),
"api_error",
null,
"internal_error"
));
}
}
private SseEmitter createStreamEmitter(UnifiedChatRequest unifiedRequest) {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
unifiedModelInvokeService.chatStream(unifiedRequest, new UnifiedChatChunkObserver() {
@Override
public void onChunk(UnifiedChatChunk chunk) {
try {
String payload = objectMapper.writeValueAsString(openAiProtocolMapper.toOpenAiChunk(chunk));
emitter.send(SseEmitter.event().data(payload));
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onComplete() {
try {
emitter.send(SseEmitter.event().data("[DONE]"));
emitter.complete();
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onError(Throwable throwable) {
log.error("chatCompletions stream error: {}", throwable.getMessage(), throwable);
emitter.completeWithError(throwable);
}
});
return emitter;
}
private ResponseEntity<String> buildJsonResponse(Object body) {
try {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(objectMapper.writeValueAsString(body));
} catch (Exception e) {
throw new RuntimeException("序列化响应失败", e);
}
}
private ResponseEntity<String> buildErrorResponse(ModelInvokeException e) {
OpenAiErrorResponse response = new OpenAiErrorResponse();
OpenAiErrorResponse.Error error = new OpenAiErrorResponse.Error();
error.setMessage(e.getMessage());
error.setType(e.getType());
error.setParam(e.getParam());
error.setCode(e.getCode());
response.setError(error);
return ResponseEntity.status(e.getStatus())
.contentType(MediaType.APPLICATION_JSON)
.body(writeJson(response));
}
private String writeJson(Object body) {
try {
return objectMapper.writeValueAsString(body);
} catch (Exception ex) {
log.error("writeJson error: {}", ex.getMessage(), ex);
return "{\"error\":{\"message\":\"响应序列化失败\",\"type\":\"api_error\",\"code\":\"serialization_error\"}}";
}
}
private ModelInvokeException mapBusinessException(BusinessException e) {
String message = StrUtil.blankToDefault(e.getMessage(), "访问令牌校验失败");
if (StrUtil.containsAnyIgnoreCase(message, "apikey 不存在", "apikey 已过期", "已禁用")) {
return ModelInvokeException.unauthorized(message);
}
if (StrUtil.containsAnyIgnoreCase(message, "无权限", "接口不存在")) {
return ModelInvokeException.forbidden(message);
}
return ModelInvokeException.badRequest(message);
}
private String resolveApiKey(String authorizationHeader) {
if (StrUtil.isBlank(authorizationHeader)) {
throw ModelInvokeException.unauthorized("Authorization 不能为空");
}
String trimmed = authorizationHeader.trim();
if (StrUtil.startWithIgnoreCase(trimmed, "Bearer ")) {
trimmed = trimmed.substring(7).trim();
}
if (StrUtil.isBlank(trimmed)) {
throw ModelInvokeException.unauthorized("Authorization 无效");
}
return trimmed;
}
}

View File

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

View File

@@ -0,0 +1,65 @@
package tech.easyflow.ai.dto;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.List;
public class ModelInvokeConfigDtos {
public static class UpdateRequest implements Serializable {
private static final long serialVersionUID = 1L;
private BigInteger id;
private String invokeCode;
private Boolean publishEnabled;
public BigInteger getId() {
return id;
}
public void setId(BigInteger id) {
this.id = id;
}
public String getInvokeCode() {
return invokeCode;
}
public void setInvokeCode(String invokeCode) {
this.invokeCode = invokeCode;
}
public Boolean getPublishEnabled() {
return publishEnabled;
}
public void setPublishEnabled(Boolean publishEnabled) {
this.publishEnabled = publishEnabled;
}
}
public static class BatchPublishRequest implements Serializable {
private static final long serialVersionUID = 1L;
private List<BigInteger> ids;
private Boolean publishEnabled;
public List<BigInteger> getIds() {
return ids;
}
public void setIds(List<BigInteger> ids) {
this.ids = ids;
}
public Boolean getPublishEnabled() {
return publishEnabled;
}
public void setPublishEnabled(Boolean publishEnabled) {
this.publishEnabled = publishEnabled;
}
}
}

View File

@@ -152,6 +152,18 @@ public class ModelBase implements Serializable {
@Column(comment = "是否支持tool消息")
private Boolean supportToolMessage;
/**
* 统一模型调用对外标识
*/
@Column(comment = "统一模型调用对外标识")
private String invokeCode;
/**
* 是否开启统一模型调用发布
*/
@Column(comment = "是否开启统一模型调用发布")
private Boolean publishEnabled;
public BigInteger getId() {
return id;
}
@@ -336,4 +348,20 @@ public class ModelBase implements Serializable {
this.supportToolMessage = supportToolMessage;
}
public String getInvokeCode() {
return invokeCode;
}
public void setInvokeCode(String invokeCode) {
this.invokeCode = invokeCode;
}
public Boolean getPublishEnabled() {
return publishEnabled;
}
public void setPublishEnabled(Boolean publishEnabled) {
this.publishEnabled = publishEnabled;
}
}

View File

@@ -0,0 +1,61 @@
package tech.easyflow.ai.invoke.exception;
public class ModelInvokeException extends RuntimeException {
private final int status;
private final String type;
private final String param;
private final String code;
public ModelInvokeException(int status, String message, String type, String param, String code) {
super(message);
this.status = status;
this.type = type;
this.param = param;
this.code = code;
}
public int getStatus() {
return status;
}
public String getType() {
return type;
}
public String getParam() {
return param;
}
public String getCode() {
return code;
}
public static ModelInvokeException badRequest(String message) {
return badRequest(message, null, "invalid_request_error");
}
public static ModelInvokeException badRequest(String message, String param, String code) {
return new ModelInvokeException(400, message, "invalid_request_error", param, code);
}
public static ModelInvokeException unauthorized(String message) {
return new ModelInvokeException(401, message, "authentication_error", null, "unauthorized");
}
public static ModelInvokeException forbidden(String message) {
return new ModelInvokeException(403, message, "permission_error", null, "forbidden");
}
public static ModelInvokeException notFound(String message) {
return new ModelInvokeException(404, message, "not_found_error", "model", "model_not_found");
}
public static ModelInvokeException badGateway(String message) {
return new ModelInvokeException(502, message, "api_error", null, "upstream_bad_gateway");
}
public static ModelInvokeException serviceUnavailable(String message) {
return new ModelInvokeException(503, message, "service_unavailable_error", null, "upstream_unavailable");
}
}

View File

@@ -0,0 +1,651 @@
package tech.easyflow.ai.invoke.mapper;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
import tech.easyflow.ai.invoke.model.UnifiedChoice;
import tech.easyflow.ai.invoke.model.UnifiedContentPart;
import tech.easyflow.ai.invoke.model.UnifiedImageUrl;
import tech.easyflow.ai.invoke.model.UnifiedMessage;
import tech.easyflow.ai.invoke.model.UnifiedResponseFormat;
import tech.easyflow.ai.invoke.model.UnifiedTool;
import tech.easyflow.ai.invoke.model.UnifiedToolCall;
import tech.easyflow.ai.invoke.model.UnifiedToolCallFunction;
import tech.easyflow.ai.invoke.model.UnifiedToolFunction;
import tech.easyflow.ai.invoke.model.UnifiedUsage;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionChunkResponse;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@Component
public class OpenAiProtocolMapper {
private static final Set<String> ROOT_FIELDS = Set.of(
"model", "messages", "stream", "temperature", "top_p", "max_tokens",
"seed", "tools", "tool_choice", "response_format"
);
private static final Set<String> MESSAGE_FIELDS = Set.of(
"role", "content", "name", "tool_call_id", "tool_calls"
);
private static final Set<String> CONTENT_PART_FIELDS = Set.of("type", "text", "image_url");
private static final Set<String> IMAGE_URL_FIELDS = Set.of("url", "detail");
private static final Set<String> TOOL_FIELDS = Set.of("type", "function");
private static final Set<String> TOOL_FUNCTION_FIELDS = Set.of("name", "description", "parameters");
private static final Set<String> RESPONSE_FORMAT_FIELDS = Set.of("type", "json_schema");
private static final Set<String> TOOL_CHOICE_FIELDS = Set.of("type", "function");
private final ObjectMapper objectMapper;
public OpenAiProtocolMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public OpenAiChatCompletionRequest readRequest(String rawBody) {
JsonNode rootNode;
try {
rootNode = objectMapper.readTree(rawBody);
} catch (JsonProcessingException e) {
throw ModelInvokeException.badRequest("请求体不是合法 JSON", null, "invalid_json");
}
if (rootNode == null || !rootNode.isObject()) {
throw ModelInvokeException.badRequest("请求体必须为 JSON 对象", null, "invalid_json");
}
validateAllowedFields(rootNode, ROOT_FIELDS, null);
validateMessages(rootNode.path("messages"));
validateTools(rootNode.path("tools"));
validateToolChoice(rootNode.path("tool_choice"));
validateResponseFormat(rootNode.path("response_format"));
try {
OpenAiChatCompletionRequest request = buildRequest(rootNode);
if (StrUtil.isBlank(request.getModel())) {
throw ModelInvokeException.badRequest("model 不能为空", "model", "model_required");
}
if (request.getMessages() == null || request.getMessages().isEmpty()) {
throw ModelInvokeException.badRequest("messages 不能为空", "messages", "messages_required");
}
return request;
} catch (IllegalArgumentException e) {
throw ModelInvokeException.badRequest(e.getMessage(), "messages", "invalid_message_content");
}
}
private OpenAiChatCompletionRequest buildRequest(JsonNode rootNode) {
OpenAiChatCompletionRequest request = new OpenAiChatCompletionRequest();
request.setModel(textValue(rootNode, "model"));
request.setMessages(buildMessages(rootNode.path("messages")));
if (rootNode.has("stream") && !rootNode.get("stream").isNull()) {
request.setStream(rootNode.get("stream").asBoolean());
}
if (rootNode.has("temperature") && !rootNode.get("temperature").isNull()) {
request.setTemperature(rootNode.get("temperature").asDouble());
}
if (rootNode.has("top_p") && !rootNode.get("top_p").isNull()) {
request.setTopP(rootNode.get("top_p").asDouble());
}
if (rootNode.has("max_tokens") && !rootNode.get("max_tokens").isNull()) {
request.setMaxTokens(rootNode.get("max_tokens").asInt());
}
if (rootNode.has("seed") && !rootNode.get("seed").isNull()) {
request.setSeed(rootNode.get("seed").asLong());
}
if (rootNode.has("tools") && rootNode.get("tools").isArray()) {
request.setTools(objectMapper.convertValue(
rootNode.get("tools"),
new TypeReference<>() {
}
));
}
if (rootNode.has("tool_choice") && !rootNode.get("tool_choice").isNull()) {
request.setToolChoice(rootNode.get("tool_choice"));
}
if (rootNode.has("response_format") && rootNode.get("response_format").isObject()) {
request.setResponseFormat(objectMapper.convertValue(
rootNode.get("response_format"),
OpenAiChatCompletionRequest.ResponseFormat.class
));
}
return request;
}
private List<OpenAiChatCompletionRequest.Message> buildMessages(JsonNode messagesNode) {
List<OpenAiChatCompletionRequest.Message> messages = new ArrayList<>();
for (JsonNode messageNode : messagesNode) {
OpenAiChatCompletionRequest.Message message = new OpenAiChatCompletionRequest.Message();
message.setRole(textValue(messageNode, "role"));
if (messageNode.has("content")) {
message.setContentNode(messageNode.get("content"));
}
message.setName(textValue(messageNode, "name"));
message.setToolCallId(textValue(messageNode, "tool_call_id"));
if (messageNode.has("tool_calls") && messageNode.get("tool_calls").isArray()) {
message.setToolCalls(objectMapper.convertValue(
messageNode.get("tool_calls"),
new TypeReference<>() {
}
));
}
messages.add(message);
}
return messages;
}
public UnifiedChatRequest toUnifiedRequest(OpenAiChatCompletionRequest request) {
UnifiedChatRequest unifiedRequest = new UnifiedChatRequest();
unifiedRequest.setModel(request.getModel());
unifiedRequest.setMessages(toUnifiedMessages(request.getMessages()));
unifiedRequest.setStream(Boolean.TRUE.equals(request.getStream()));
unifiedRequest.setTemperature(request.getTemperature());
unifiedRequest.setTopP(request.getTopP());
unifiedRequest.setMaxTokens(request.getMaxTokens());
unifiedRequest.setSeed(request.getSeed());
unifiedRequest.setTools(toUnifiedTools(request.getTools()));
unifiedRequest.setToolChoice(request.getToolChoice());
unifiedRequest.setResponseFormat(toUnifiedResponseFormat(request.getResponseFormat()));
return unifiedRequest;
}
public OpenAiChatCompletionRequest toOpenAiRequest(UnifiedChatRequest request) {
OpenAiChatCompletionRequest openAiRequest = new OpenAiChatCompletionRequest();
openAiRequest.setModel(request.getModel());
openAiRequest.setMessages(toOpenAiMessages(request.getMessages()));
openAiRequest.setStream(request.getStream());
openAiRequest.setTemperature(request.getTemperature());
openAiRequest.setTopP(request.getTopP());
openAiRequest.setMaxTokens(request.getMaxTokens());
openAiRequest.setSeed(request.getSeed());
openAiRequest.setTools(toOpenAiTools(request.getTools()));
openAiRequest.setToolChoice(request.getToolChoice());
openAiRequest.setResponseFormat(toOpenAiResponseFormat(request.getResponseFormat()));
return openAiRequest;
}
public UnifiedChatResponse toUnifiedResponse(OpenAiChatCompletionResponse response) {
UnifiedChatResponse unifiedResponse = new UnifiedChatResponse();
unifiedResponse.setId(response.getId());
unifiedResponse.setObject(response.getObject());
unifiedResponse.setCreated(response.getCreated());
unifiedResponse.setModel(response.getModel());
unifiedResponse.setChoices(toUnifiedChoices(response.getChoices(), false));
unifiedResponse.setUsage(toUnifiedUsage(response.getUsage()));
return unifiedResponse;
}
public UnifiedChatChunk toUnifiedChunk(OpenAiChatCompletionChunkResponse chunk) {
UnifiedChatChunk unifiedChunk = new UnifiedChatChunk();
unifiedChunk.setId(chunk.getId());
unifiedChunk.setObject(chunk.getObject());
unifiedChunk.setCreated(chunk.getCreated());
unifiedChunk.setModel(chunk.getModel());
unifiedChunk.setChoices(toUnifiedChunkChoices(chunk.getChoices()));
unifiedChunk.setUsage(toUnifiedUsage(chunk.getUsage()));
return unifiedChunk;
}
public OpenAiChatCompletionResponse toOpenAiResponse(UnifiedChatResponse response) {
OpenAiChatCompletionResponse openAiResponse = new OpenAiChatCompletionResponse();
openAiResponse.setId(response.getId());
openAiResponse.setObject(response.getObject());
openAiResponse.setCreated(response.getCreated());
openAiResponse.setModel(response.getModel());
openAiResponse.setChoices(toOpenAiChoices(response.getChoices(), false));
openAiResponse.setUsage(toOpenAiUsage(response.getUsage()));
return openAiResponse;
}
public OpenAiChatCompletionChunkResponse toOpenAiChunk(UnifiedChatChunk chunk) {
OpenAiChatCompletionChunkResponse openAiChunk = new OpenAiChatCompletionChunkResponse();
openAiChunk.setId(chunk.getId());
openAiChunk.setObject(chunk.getObject());
openAiChunk.setCreated(chunk.getCreated());
openAiChunk.setModel(chunk.getModel());
openAiChunk.setChoices(toOpenAiChunkChoices(chunk.getChoices()));
openAiChunk.setUsage(toOpenAiUsage(chunk.getUsage()));
return openAiChunk;
}
private void validateMessages(JsonNode messagesNode) {
if (messagesNode == null || !messagesNode.isArray() || messagesNode.isEmpty()) {
throw ModelInvokeException.badRequest("messages 必须为非空数组", "messages", "messages_required");
}
for (int i = 0; i < messagesNode.size(); i++) {
JsonNode messageNode = messagesNode.get(i);
if (!messageNode.isObject()) {
throw ModelInvokeException.badRequest("messages[" + i + "] 必须为对象", "messages", "invalid_message");
}
validateAllowedFields(messageNode, MESSAGE_FIELDS, "messages[" + i + "]");
String role = textValue(messageNode, "role");
if (!Set.of("system", "developer", "user", "assistant", "tool").contains(role)) {
throw ModelInvokeException.badRequest("messages[" + i + "].role 不受支持", "messages", "invalid_message_role");
}
JsonNode contentNode = messageNode.get("content");
if (contentNode != null && !contentNode.isNull() && !contentNode.isTextual() && !contentNode.isArray()) {
throw ModelInvokeException.badRequest("messages[" + i + "].content 仅支持字符串或数组", "messages", "invalid_message_content");
}
if (contentNode != null && contentNode.isArray()) {
validateContentParts(contentNode, "messages[" + i + "].content");
}
}
}
private void validateContentParts(JsonNode contentNode, String path) {
for (int i = 0; i < contentNode.size(); i++) {
JsonNode partNode = contentNode.get(i);
if (!partNode.isObject()) {
throw ModelInvokeException.badRequest(path + "[" + i + "] 必须为对象", "messages", "invalid_content_part");
}
validateAllowedFields(partNode, CONTENT_PART_FIELDS, path + "[" + i + "]");
String type = textValue(partNode, "type");
if (!Set.of("text", "image_url").contains(type)) {
throw ModelInvokeException.badRequest(path + "[" + i + "].type 不受支持", "messages", "content_part_not_supported");
}
if ("text".equals(type) && StrUtil.isBlank(textValue(partNode, "text"))) {
throw ModelInvokeException.badRequest(path + "[" + i + "].text 不能为空", "messages", "content_text_required");
}
if ("image_url".equals(type)) {
JsonNode imageUrlNode = partNode.get("image_url");
if (imageUrlNode == null || !imageUrlNode.isObject()) {
throw ModelInvokeException.badRequest(path + "[" + i + "].image_url 必须为对象", "messages", "image_url_required");
}
validateAllowedFields(imageUrlNode, IMAGE_URL_FIELDS, path + "[" + i + "].image_url");
if (StrUtil.isBlank(textValue(imageUrlNode, "url"))) {
throw ModelInvokeException.badRequest(path + "[" + i + "].image_url.url 不能为空", "messages", "image_url_required");
}
}
}
}
private void validateTools(JsonNode toolsNode) {
if (isAbsentNode(toolsNode)) {
return;
}
if (!toolsNode.isArray()) {
throw ModelInvokeException.badRequest("tools 必须为数组", "tools", "invalid_tools");
}
for (int i = 0; i < toolsNode.size(); i++) {
JsonNode toolNode = toolsNode.get(i);
if (!toolNode.isObject()) {
throw ModelInvokeException.badRequest("tools[" + i + "] 必须为对象", "tools", "invalid_tools");
}
validateAllowedFields(toolNode, TOOL_FIELDS, "tools[" + i + "]");
JsonNode functionNode = toolNode.get("function");
if (functionNode == null || !functionNode.isObject()) {
throw ModelInvokeException.badRequest("tools[" + i + "].function 必须为对象", "tools", "invalid_tool_function");
}
validateAllowedFields(functionNode, TOOL_FUNCTION_FIELDS, "tools[" + i + "].function");
if (StrUtil.isBlank(textValue(functionNode, "name"))) {
throw ModelInvokeException.badRequest("tools[" + i + "].function.name 不能为空", "tools", "tool_name_required");
}
}
}
private void validateToolChoice(JsonNode toolChoiceNode) {
if (isAbsentNode(toolChoiceNode)) {
return;
}
if (toolChoiceNode.isTextual()) {
return;
}
if (!toolChoiceNode.isObject()) {
throw ModelInvokeException.badRequest("tool_choice 仅支持字符串或对象", "tool_choice", "invalid_tool_choice");
}
validateAllowedFields(toolChoiceNode, TOOL_CHOICE_FIELDS, "tool_choice");
}
private void validateResponseFormat(JsonNode responseFormatNode) {
if (isAbsentNode(responseFormatNode)) {
return;
}
if (!responseFormatNode.isObject()) {
throw ModelInvokeException.badRequest("response_format 必须为对象", "response_format", "invalid_response_format");
}
validateAllowedFields(responseFormatNode, RESPONSE_FORMAT_FIELDS, "response_format");
if (StrUtil.isBlank(textValue(responseFormatNode, "type"))) {
throw ModelInvokeException.badRequest("response_format.type 不能为空", "response_format", "response_format_type_required");
}
}
private boolean isAbsentNode(JsonNode node) {
return node == null || node.isNull() || node.isMissingNode();
}
private void validateAllowedFields(JsonNode node, Set<String> allowedFields, String path) {
Iterator<String> fieldNames = node.fieldNames();
Set<String> unsupportedFields = new LinkedHashSet<>();
while (fieldNames.hasNext()) {
String fieldName = fieldNames.next();
if (!allowedFields.contains(fieldName)) {
unsupportedFields.add(path == null ? fieldName : path + "." + fieldName);
}
}
if (!unsupportedFields.isEmpty()) {
String field = unsupportedFields.iterator().next();
throw ModelInvokeException.badRequest("存在未支持字段: " + field, field, "unsupported_field");
}
}
private String textValue(JsonNode node, String fieldName) {
JsonNode fieldNode = node.get(fieldName);
if (fieldNode == null || fieldNode.isNull()) {
return null;
}
return fieldNode.asText();
}
private List<UnifiedMessage> toUnifiedMessages(List<OpenAiChatCompletionRequest.Message> messages) {
if (messages == null) {
return null;
}
List<UnifiedMessage> result = new ArrayList<>();
for (OpenAiChatCompletionRequest.Message message : messages) {
UnifiedMessage unifiedMessage = new UnifiedMessage();
unifiedMessage.setRole(message.getRole());
unifiedMessage.setContent(message.getContent());
unifiedMessage.setContentParts(toUnifiedContentParts(message.getContentParts()));
unifiedMessage.setName(message.getName());
unifiedMessage.setToolCallId(message.getToolCallId());
unifiedMessage.setToolCalls(toUnifiedToolCalls(message.getToolCalls()));
result.add(unifiedMessage);
}
return result;
}
private List<OpenAiChatCompletionRequest.Message> toOpenAiMessages(List<UnifiedMessage> messages) {
if (messages == null) {
return null;
}
List<OpenAiChatCompletionRequest.Message> result = new ArrayList<>();
for (UnifiedMessage message : messages) {
OpenAiChatCompletionRequest.Message openAiMessage = new OpenAiChatCompletionRequest.Message();
openAiMessage.setRole(message.getRole());
if (message.getContentParts() != null && !message.getContentParts().isEmpty()) {
ObjectNode contentNode = objectMapper.createObjectNode();
contentNode.set("content", objectMapper.valueToTree(toOpenAiContentParts(message.getContentParts())));
openAiMessage.setContentNode(contentNode.get("content"));
} else {
ObjectNode contentNode = objectMapper.createObjectNode();
if (message.getContent() == null) {
contentNode.putNull("content");
} else {
contentNode.put("content", message.getContent());
}
openAiMessage.setContentNode(contentNode.get("content"));
}
openAiMessage.setName(message.getName());
openAiMessage.setToolCallId(message.getToolCallId());
openAiMessage.setToolCalls(toOpenAiToolCalls(message.getToolCalls()));
result.add(openAiMessage);
}
return result;
}
private List<UnifiedContentPart> toUnifiedContentParts(List<OpenAiChatCompletionRequest.ContentPart> contentParts) {
if (contentParts == null) {
return null;
}
List<UnifiedContentPart> result = new ArrayList<>();
for (OpenAiChatCompletionRequest.ContentPart contentPart : contentParts) {
UnifiedContentPart unifiedContentPart = new UnifiedContentPart();
unifiedContentPart.setType(contentPart.getType());
unifiedContentPart.setText(contentPart.getText());
if (contentPart.getImageUrl() != null) {
UnifiedImageUrl imageUrl = new UnifiedImageUrl();
imageUrl.setUrl(contentPart.getImageUrl().getUrl());
imageUrl.setDetail(contentPart.getImageUrl().getDetail());
unifiedContentPart.setImageUrl(imageUrl);
}
result.add(unifiedContentPart);
}
return result;
}
private List<OpenAiChatCompletionRequest.ContentPart> toOpenAiContentParts(List<UnifiedContentPart> contentParts) {
if (contentParts == null) {
return null;
}
List<OpenAiChatCompletionRequest.ContentPart> result = new ArrayList<>();
for (UnifiedContentPart contentPart : contentParts) {
OpenAiChatCompletionRequest.ContentPart openAiPart = new OpenAiChatCompletionRequest.ContentPart();
openAiPart.setType(contentPart.getType());
openAiPart.setText(contentPart.getText());
if (contentPart.getImageUrl() != null) {
OpenAiChatCompletionRequest.ImageUrl imageUrl = new OpenAiChatCompletionRequest.ImageUrl();
imageUrl.setUrl(contentPart.getImageUrl().getUrl());
imageUrl.setDetail(contentPart.getImageUrl().getDetail());
openAiPart.setImageUrl(imageUrl);
}
result.add(openAiPart);
}
return result;
}
private List<UnifiedTool> toUnifiedTools(List<OpenAiChatCompletionRequest.Tool> tools) {
if (tools == null) {
return null;
}
List<UnifiedTool> result = new ArrayList<>();
for (OpenAiChatCompletionRequest.Tool tool : tools) {
UnifiedTool unifiedTool = new UnifiedTool();
unifiedTool.setType(tool.getType());
if (tool.getFunction() != null) {
UnifiedToolFunction function = new UnifiedToolFunction();
function.setName(tool.getFunction().getName());
function.setDescription(tool.getFunction().getDescription());
function.setParameters(tool.getFunction().getParameters());
unifiedTool.setFunction(function);
}
result.add(unifiedTool);
}
return result;
}
private List<OpenAiChatCompletionRequest.Tool> toOpenAiTools(List<UnifiedTool> tools) {
if (tools == null) {
return null;
}
List<OpenAiChatCompletionRequest.Tool> result = new ArrayList<>();
for (UnifiedTool tool : tools) {
OpenAiChatCompletionRequest.Tool openAiTool = new OpenAiChatCompletionRequest.Tool();
openAiTool.setType(tool.getType());
if (tool.getFunction() != null) {
OpenAiChatCompletionRequest.ToolFunction function = new OpenAiChatCompletionRequest.ToolFunction();
function.setName(tool.getFunction().getName());
function.setDescription(tool.getFunction().getDescription());
function.setParameters(tool.getFunction().getParameters());
openAiTool.setFunction(function);
}
result.add(openAiTool);
}
return result;
}
private UnifiedResponseFormat toUnifiedResponseFormat(OpenAiChatCompletionRequest.ResponseFormat responseFormat) {
if (responseFormat == null) {
return null;
}
UnifiedResponseFormat unifiedResponseFormat = new UnifiedResponseFormat();
unifiedResponseFormat.setType(responseFormat.getType());
unifiedResponseFormat.setJsonSchema(responseFormat.getJsonSchema());
return unifiedResponseFormat;
}
private OpenAiChatCompletionRequest.ResponseFormat toOpenAiResponseFormat(UnifiedResponseFormat responseFormat) {
if (responseFormat == null) {
return null;
}
OpenAiChatCompletionRequest.ResponseFormat openAiResponseFormat = new OpenAiChatCompletionRequest.ResponseFormat();
openAiResponseFormat.setType(responseFormat.getType());
openAiResponseFormat.setJsonSchema(responseFormat.getJsonSchema());
return openAiResponseFormat;
}
private List<UnifiedChoice> toUnifiedChoices(List<OpenAiChatCompletionResponse.Choice> choices, boolean delta) {
if (choices == null) {
return null;
}
List<UnifiedChoice> result = new ArrayList<>();
for (OpenAiChatCompletionResponse.Choice choice : choices) {
UnifiedChoice unifiedChoice = new UnifiedChoice();
unifiedChoice.setIndex(choice.getIndex());
UnifiedMessage message = new UnifiedMessage();
if (choice.getMessage() != null) {
message.setRole(choice.getMessage().getRole());
message.setContent(choice.getMessage().getContent());
message.setToolCallId(choice.getMessage().getToolCallId());
message.setToolCalls(toUnifiedToolCalls(choice.getMessage().getToolCalls()));
}
if (delta) {
unifiedChoice.setDelta(message);
} else {
unifiedChoice.setMessage(message);
}
unifiedChoice.setFinishReason(choice.getFinishReason());
result.add(unifiedChoice);
}
return result;
}
private List<UnifiedChoice> toUnifiedChunkChoices(List<OpenAiChatCompletionChunkResponse.Choice> choices) {
if (choices == null) {
return null;
}
List<UnifiedChoice> result = new ArrayList<>();
for (OpenAiChatCompletionChunkResponse.Choice choice : choices) {
UnifiedChoice unifiedChoice = new UnifiedChoice();
unifiedChoice.setIndex(choice.getIndex());
UnifiedMessage delta = new UnifiedMessage();
if (choice.getDelta() != null) {
delta.setRole(choice.getDelta().getRole());
delta.setContent(choice.getDelta().getContent());
delta.setToolCalls(toUnifiedToolCalls(choice.getDelta().getToolCalls()));
}
unifiedChoice.setDelta(delta);
unifiedChoice.setFinishReason(choice.getFinishReason());
result.add(unifiedChoice);
}
return result;
}
private List<OpenAiChatCompletionResponse.Choice> toOpenAiChoices(List<UnifiedChoice> choices, boolean delta) {
if (choices == null) {
return null;
}
List<OpenAiChatCompletionResponse.Choice> result = new ArrayList<>();
for (UnifiedChoice choice : choices) {
OpenAiChatCompletionResponse.Choice openAiChoice = new OpenAiChatCompletionResponse.Choice();
openAiChoice.setIndex(choice.getIndex());
UnifiedMessage source = delta ? choice.getDelta() : choice.getMessage();
if (source != null) {
OpenAiChatCompletionResponse.Message message = new OpenAiChatCompletionResponse.Message();
message.setRole(source.getRole());
message.setContent(source.getContent());
message.setToolCallId(source.getToolCallId());
message.setToolCalls(toOpenAiToolCalls(source.getToolCalls()));
openAiChoice.setMessage(message);
}
openAiChoice.setFinishReason(choice.getFinishReason());
result.add(openAiChoice);
}
return result;
}
private List<OpenAiChatCompletionChunkResponse.Choice> toOpenAiChunkChoices(List<UnifiedChoice> choices) {
if (choices == null) {
return null;
}
List<OpenAiChatCompletionChunkResponse.Choice> result = new ArrayList<>();
for (UnifiedChoice choice : choices) {
OpenAiChatCompletionChunkResponse.Choice openAiChoice = new OpenAiChatCompletionChunkResponse.Choice();
openAiChoice.setIndex(choice.getIndex());
if (choice.getDelta() != null) {
OpenAiChatCompletionChunkResponse.Delta delta = new OpenAiChatCompletionChunkResponse.Delta();
delta.setRole(choice.getDelta().getRole());
delta.setContent(choice.getDelta().getContent());
delta.setToolCalls(toOpenAiToolCalls(choice.getDelta().getToolCalls()));
openAiChoice.setDelta(delta);
}
openAiChoice.setFinishReason(choice.getFinishReason());
result.add(openAiChoice);
}
return result;
}
private List<UnifiedToolCall> toUnifiedToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
if (toolCalls == null) {
return null;
}
List<UnifiedToolCall> result = new ArrayList<>();
for (OpenAiChatCompletionResponse.ToolCall toolCall : toolCalls) {
UnifiedToolCall unifiedToolCall = new UnifiedToolCall();
unifiedToolCall.setIndex(toolCall.getIndex());
unifiedToolCall.setId(toolCall.getId());
unifiedToolCall.setType(toolCall.getType());
if (toolCall.getFunction() != null) {
UnifiedToolCallFunction function = new UnifiedToolCallFunction();
function.setName(toolCall.getFunction().getName());
function.setArguments(toolCall.getFunction().getArguments());
unifiedToolCall.setFunction(function);
}
result.add(unifiedToolCall);
}
return result;
}
private List<OpenAiChatCompletionResponse.ToolCall> toOpenAiToolCalls(List<UnifiedToolCall> toolCalls) {
if (toolCalls == null) {
return null;
}
List<OpenAiChatCompletionResponse.ToolCall> result = new ArrayList<>();
for (UnifiedToolCall toolCall : toolCalls) {
OpenAiChatCompletionResponse.ToolCall openAiToolCall = new OpenAiChatCompletionResponse.ToolCall();
openAiToolCall.setIndex(toolCall.getIndex());
openAiToolCall.setId(toolCall.getId());
openAiToolCall.setType(toolCall.getType());
if (toolCall.getFunction() != null) {
OpenAiChatCompletionResponse.ToolCallFunction function = new OpenAiChatCompletionResponse.ToolCallFunction();
function.setName(toolCall.getFunction().getName());
function.setArguments(toolCall.getFunction().getArguments());
openAiToolCall.setFunction(function);
}
result.add(openAiToolCall);
}
return result;
}
private UnifiedUsage toUnifiedUsage(OpenAiChatCompletionResponse.Usage usage) {
if (usage == null) {
return null;
}
UnifiedUsage unifiedUsage = new UnifiedUsage();
unifiedUsage.setPromptTokens(usage.getPromptTokens());
unifiedUsage.setCompletionTokens(usage.getCompletionTokens());
unifiedUsage.setTotalTokens(usage.getTotalTokens());
return unifiedUsage;
}
private OpenAiChatCompletionResponse.Usage toOpenAiUsage(UnifiedUsage usage) {
if (usage == null) {
return null;
}
OpenAiChatCompletionResponse.Usage openAiUsage = new OpenAiChatCompletionResponse.Usage();
openAiUsage.setPromptTokens(usage.getPromptTokens());
openAiUsage.setCompletionTokens(usage.getCompletionTokens());
openAiUsage.setTotalTokens(usage.getTotalTokens());
return openAiUsage;
}
}

View File

@@ -0,0 +1,61 @@
package tech.easyflow.ai.invoke.model;
import java.util.List;
public class UnifiedChatChunk {
private String id;
private String object;
private Long created;
private String model;
private List<UnifiedChoice> choices;
private UnifiedUsage usage;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObject() {
return object;
}
public void setObject(String object) {
this.object = object;
}
public Long getCreated() {
return created;
}
public void setCreated(Long created) {
this.created = created;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<UnifiedChoice> getChoices() {
return choices;
}
public void setChoices(List<UnifiedChoice> choices) {
this.choices = choices;
}
public UnifiedUsage getUsage() {
return usage;
}
public void setUsage(UnifiedUsage usage) {
this.usage = usage;
}
}

View File

@@ -0,0 +1,99 @@
package tech.easyflow.ai.invoke.model;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
public class UnifiedChatRequest {
private String model;
private List<UnifiedMessage> messages;
private Boolean stream;
private Double temperature;
private Double topP;
private Integer maxTokens;
private Long seed;
private List<UnifiedTool> tools;
private JsonNode toolChoice;
private UnifiedResponseFormat responseFormat;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<UnifiedMessage> getMessages() {
return messages;
}
public void setMessages(List<UnifiedMessage> messages) {
this.messages = messages;
}
public Boolean getStream() {
return stream;
}
public void setStream(Boolean stream) {
this.stream = stream;
}
public Double getTemperature() {
return temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
public Double getTopP() {
return topP;
}
public void setTopP(Double topP) {
this.topP = topP;
}
public Integer getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
public Long getSeed() {
return seed;
}
public void setSeed(Long seed) {
this.seed = seed;
}
public List<UnifiedTool> getTools() {
return tools;
}
public void setTools(List<UnifiedTool> tools) {
this.tools = tools;
}
public JsonNode getToolChoice() {
return toolChoice;
}
public void setToolChoice(JsonNode toolChoice) {
this.toolChoice = toolChoice;
}
public UnifiedResponseFormat getResponseFormat() {
return responseFormat;
}
public void setResponseFormat(UnifiedResponseFormat responseFormat) {
this.responseFormat = responseFormat;
}
}

View File

@@ -0,0 +1,61 @@
package tech.easyflow.ai.invoke.model;
import java.util.List;
public class UnifiedChatResponse {
private String id;
private String object;
private Long created;
private String model;
private List<UnifiedChoice> choices;
private UnifiedUsage usage;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObject() {
return object;
}
public void setObject(String object) {
this.object = object;
}
public Long getCreated() {
return created;
}
public void setCreated(Long created) {
this.created = created;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<UnifiedChoice> getChoices() {
return choices;
}
public void setChoices(List<UnifiedChoice> choices) {
this.choices = choices;
}
public UnifiedUsage getUsage() {
return usage;
}
public void setUsage(UnifiedUsage usage) {
this.usage = usage;
}
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedChoice {
private Integer index;
private UnifiedMessage message;
private UnifiedMessage delta;
private String finishReason;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public UnifiedMessage getMessage() {
return message;
}
public void setMessage(UnifiedMessage message) {
this.message = message;
}
public UnifiedMessage getDelta() {
return delta;
}
public void setDelta(UnifiedMessage delta) {
this.delta = delta;
}
public String getFinishReason() {
return finishReason;
}
public void setFinishReason(String finishReason) {
this.finishReason = finishReason;
}
}

View File

@@ -0,0 +1,32 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedContentPart {
private String type;
private String text;
private UnifiedImageUrl imageUrl;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public UnifiedImageUrl getImageUrl() {
return imageUrl;
}
public void setImageUrl(UnifiedImageUrl imageUrl) {
this.imageUrl = imageUrl;
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedImageUrl {
private String url;
private String detail;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
}

View File

@@ -0,0 +1,61 @@
package tech.easyflow.ai.invoke.model;
import java.util.List;
public class UnifiedMessage {
private String role;
private String content;
private List<UnifiedContentPart> contentParts;
private String name;
private String toolCallId;
private List<UnifiedToolCall> toolCalls;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public List<UnifiedContentPart> getContentParts() {
return contentParts;
}
public void setContentParts(List<UnifiedContentPart> contentParts) {
this.contentParts = contentParts;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getToolCallId() {
return toolCallId;
}
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
public List<UnifiedToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<UnifiedToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
}

View File

@@ -0,0 +1,25 @@
package tech.easyflow.ai.invoke.model;
import com.fasterxml.jackson.databind.JsonNode;
public class UnifiedResponseFormat {
private String type;
private JsonNode jsonSchema;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public JsonNode getJsonSchema() {
return jsonSchema;
}
public void setJsonSchema(JsonNode jsonSchema) {
this.jsonSchema = jsonSchema;
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedTool {
private String type;
private UnifiedToolFunction function;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public UnifiedToolFunction getFunction() {
return function;
}
public void setFunction(UnifiedToolFunction function) {
this.function = function;
}
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedToolCall {
private Integer index;
private String id;
private String type;
private UnifiedToolCallFunction function;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public UnifiedToolCallFunction getFunction() {
return function;
}
public void setFunction(UnifiedToolCallFunction function) {
this.function = function;
}
}

View File

@@ -0,0 +1,23 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedToolCallFunction {
private String name;
private String arguments;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getArguments() {
return arguments;
}
public void setArguments(String arguments) {
this.arguments = arguments;
}
}

View File

@@ -0,0 +1,34 @@
package tech.easyflow.ai.invoke.model;
import com.fasterxml.jackson.databind.JsonNode;
public class UnifiedToolFunction {
private String name;
private String description;
private JsonNode parameters;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public JsonNode getParameters() {
return parameters;
}
public void setParameters(JsonNode parameters) {
this.parameters = parameters;
}
}

View File

@@ -0,0 +1,32 @@
package tech.easyflow.ai.invoke.model;
public class UnifiedUsage {
private Integer promptTokens;
private Integer completionTokens;
private Integer totalTokens;
public Integer getPromptTokens() {
return promptTokens;
}
public void setPromptTokens(Integer promptTokens) {
this.promptTokens = promptTokens;
}
public Integer getCompletionTokens() {
return completionTokens;
}
public void setCompletionTokens(Integer completionTokens) {
this.completionTokens = completionTokens;
}
public Integer getTotalTokens() {
return totalTokens;
}
public void setTotalTokens(Integer totalTokens) {
this.totalTokens = totalTokens;
}
}

View File

@@ -0,0 +1,134 @@
package tech.easyflow.ai.invoke.protocol.openai;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpenAiChatCompletionChunkResponse {
private String id;
private String object;
private Long created;
private String model;
private List<Choice> choices;
private OpenAiChatCompletionResponse.Usage usage;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObject() {
return object;
}
public void setObject(String object) {
this.object = object;
}
public Long getCreated() {
return created;
}
public void setCreated(Long created) {
this.created = created;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<Choice> getChoices() {
return choices;
}
public void setChoices(List<Choice> choices) {
this.choices = choices;
}
public OpenAiChatCompletionResponse.Usage getUsage() {
return usage;
}
public void setUsage(OpenAiChatCompletionResponse.Usage usage) {
this.usage = usage;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Choice {
private Integer index;
private Delta delta;
@JSONField(name = "finish_reason")
@JsonProperty("finish_reason")
private String finishReason;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public Delta getDelta() {
return delta;
}
public void setDelta(Delta delta) {
this.delta = delta;
}
public String getFinishReason() {
return finishReason;
}
public void setFinishReason(String finishReason) {
this.finishReason = finishReason;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Delta {
private String role;
private String content;
@JSONField(name = "tool_calls")
@JsonProperty("tool_calls")
private List<OpenAiChatCompletionResponse.ToolCall> toolCalls;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public List<OpenAiChatCompletionResponse.ToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
}
}

View File

@@ -0,0 +1,335 @@
package tech.easyflow.ai.invoke.protocol.openai;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpenAiChatCompletionRequest {
private String model;
private List<Message> messages;
private Boolean stream;
private Double temperature;
@JSONField(name = "top_p")
@JsonProperty("top_p")
private Double topP;
@JSONField(name = "max_tokens")
@JsonProperty("max_tokens")
private Integer maxTokens;
private Long seed;
private List<Tool> tools;
@JSONField(name = "tool_choice")
@JsonProperty("tool_choice")
private JsonNode toolChoice;
@JSONField(name = "response_format")
@JsonProperty("response_format")
private ResponseFormat responseFormat;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<Message> getMessages() {
return messages;
}
public void setMessages(List<Message> messages) {
this.messages = messages;
}
public Boolean getStream() {
return stream;
}
public void setStream(Boolean stream) {
this.stream = stream;
}
public Double getTemperature() {
return temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
public Double getTopP() {
return topP;
}
public void setTopP(Double topP) {
this.topP = topP;
}
public Integer getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
public Long getSeed() {
return seed;
}
public void setSeed(Long seed) {
this.seed = seed;
}
public List<Tool> getTools() {
return tools;
}
public void setTools(List<Tool> tools) {
this.tools = tools;
}
public JsonNode getToolChoice() {
return toolChoice;
}
public void setToolChoice(JsonNode toolChoice) {
this.toolChoice = toolChoice;
}
public ResponseFormat getResponseFormat() {
return responseFormat;
}
public void setResponseFormat(ResponseFormat responseFormat) {
this.responseFormat = responseFormat;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Message {
private String role;
private String content;
private List<ContentPart> contentParts;
private String name;
@JSONField(name = "tool_call_id")
@JsonProperty("tool_call_id")
private String toolCallId;
@JSONField(name = "tool_calls")
@JsonProperty("tool_calls")
private List<OpenAiChatCompletionResponse.ToolCall> toolCalls;
@JsonProperty("content")
public void setContentNode(JsonNode contentNode) {
if (contentNode == null || contentNode.isNull()) {
this.content = null;
this.contentParts = null;
return;
}
if (contentNode.isTextual()) {
this.content = contentNode.asText();
this.contentParts = null;
return;
}
if (contentNode.isArray()) {
this.content = null;
this.contentParts = OpenAiJsonSupport.convertContentParts(contentNode);
return;
}
throw new IllegalArgumentException("message.content 仅支持字符串或数组");
}
@JSONField(name = "content")
@JsonProperty("content")
public Object getContentNode() {
if (contentParts != null) {
return contentParts;
}
return content;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@JsonIgnore
public String getContent() {
return content;
}
@JsonIgnore
public List<ContentPart> getContentParts() {
return contentParts;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getToolCallId() {
return toolCallId;
}
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
public List<OpenAiChatCompletionResponse.ToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<OpenAiChatCompletionResponse.ToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ContentPart {
private String type;
private String text;
@JSONField(name = "image_url")
@JsonProperty("image_url")
private ImageUrl imageUrl;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public ImageUrl getImageUrl() {
return imageUrl;
}
public void setImageUrl(ImageUrl imageUrl) {
this.imageUrl = imageUrl;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ImageUrl {
private String url;
private String detail;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Tool {
private String type;
private ToolFunction function;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ToolFunction getFunction() {
return function;
}
public void setFunction(ToolFunction function) {
this.function = function;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ToolFunction {
private String name;
private String description;
private JsonNode parameters;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public JsonNode getParameters() {
return parameters;
}
public void setParameters(JsonNode parameters) {
this.parameters = parameters;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ResponseFormat {
private String type;
@JSONField(name = "json_schema")
@JsonProperty("json_schema")
private JsonNode jsonSchema;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public JsonNode getJsonSchema() {
return jsonSchema;
}
public void setJsonSchema(JsonNode jsonSchema) {
this.jsonSchema = jsonSchema;
}
}
}

View File

@@ -0,0 +1,247 @@
package tech.easyflow.ai.invoke.protocol.openai;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class OpenAiChatCompletionResponse {
private String id;
private String object;
private Long created;
private String model;
private List<Choice> choices;
private Usage usage;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObject() {
return object;
}
public void setObject(String object) {
this.object = object;
}
public Long getCreated() {
return created;
}
public void setCreated(Long created) {
this.created = created;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<Choice> getChoices() {
return choices;
}
public void setChoices(List<Choice> choices) {
this.choices = choices;
}
public Usage getUsage() {
return usage;
}
public void setUsage(Usage usage) {
this.usage = usage;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Choice {
private Integer index;
private Message message;
@JSONField(name = "finish_reason")
@JsonProperty("finish_reason")
private String finishReason;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public Message getMessage() {
return message;
}
public void setMessage(Message message) {
this.message = message;
}
public String getFinishReason() {
return finishReason;
}
public void setFinishReason(String finishReason) {
this.finishReason = finishReason;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Message {
private String role;
private String content;
@JSONField(name = "tool_call_id")
@JsonProperty("tool_call_id")
private String toolCallId;
@JSONField(name = "tool_calls")
@JsonProperty("tool_calls")
private List<ToolCall> toolCalls;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getToolCallId() {
return toolCallId;
}
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
public List<ToolCall> getToolCalls() {
return toolCalls;
}
public void setToolCalls(List<ToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ToolCall {
private Integer index;
private String id;
private String type;
private ToolCallFunction function;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public ToolCallFunction getFunction() {
return function;
}
public void setFunction(ToolCallFunction function) {
this.function = function;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ToolCallFunction {
private String name;
private String arguments;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getArguments() {
return arguments;
}
public void setArguments(String arguments) {
this.arguments = arguments;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Usage {
@JSONField(name = "prompt_tokens")
@JsonProperty("prompt_tokens")
private Integer promptTokens;
@JSONField(name = "completion_tokens")
@JsonProperty("completion_tokens")
private Integer completionTokens;
@JSONField(name = "total_tokens")
@JsonProperty("total_tokens")
private Integer totalTokens;
public Integer getPromptTokens() {
return promptTokens;
}
public void setPromptTokens(Integer promptTokens) {
this.promptTokens = promptTokens;
}
public Integer getCompletionTokens() {
return completionTokens;
}
public void setCompletionTokens(Integer completionTokens) {
this.completionTokens = completionTokens;
}
public Integer getTotalTokens() {
return totalTokens;
}
public void setTotalTokens(Integer totalTokens) {
this.totalTokens = totalTokens;
}
}
}

View File

@@ -0,0 +1,54 @@
package tech.easyflow.ai.invoke.protocol.openai;
public class OpenAiErrorResponse {
private Error error;
public Error getError() {
return error;
}
public void setError(Error error) {
this.error = error;
}
public static class Error {
private String message;
private String type;
private String param;
private String code;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
}

View File

@@ -0,0 +1,20 @@
package tech.easyflow.ai.invoke.protocol.openai;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
final class OpenAiJsonSupport {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private OpenAiJsonSupport() {
}
static List<OpenAiChatCompletionRequest.ContentPart> convertContentParts(JsonNode node) {
return OBJECT_MAPPER.convertValue(node, new TypeReference<>() {
});
}
}

View File

@@ -0,0 +1,14 @@
package tech.easyflow.ai.invoke.provider;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
public interface ModelProviderGateway {
boolean supports(String providerType);
UnifiedChatResponse chat(Model model, UnifiedChatRequest request);
void chatStream(Model model, UnifiedChatRequest request, UnifiedChatChunkObserver observer);
}

View File

@@ -0,0 +1,27 @@
package tech.easyflow.ai.invoke.provider;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
import java.util.List;
@Component
public class ModelProviderGatewayRegistry {
private final List<ModelProviderGateway> gateways;
public ModelProviderGatewayRegistry(List<ModelProviderGateway> gateways) {
this.gateways = gateways;
}
public ModelProviderGateway getGateway(String providerType) {
return gateways.stream()
.filter(gateway -> gateway.supports(providerType))
.findFirst()
.orElseThrow(() -> ModelInvokeException.badRequest(
"当前 providerType 暂不支持统一模型调用: " + providerType,
"model",
"provider_not_supported"
));
}
}

View File

@@ -0,0 +1,14 @@
package tech.easyflow.ai.invoke.provider;
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
public interface UnifiedChatChunkObserver {
void onChunk(UnifiedChatChunk chunk);
default void onComplete() {
}
default void onError(Throwable throwable) {
}
}

View File

@@ -0,0 +1,129 @@
package tech.easyflow.ai.invoke.provider.base;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
import tech.easyflow.ai.invoke.model.UnifiedChatChunk;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionChunkResponse;
import tech.easyflow.common.util.OkHttpClientUtil;
import java.io.IOException;
import java.util.Objects;
public abstract class AbstractOpenAiCompatibleGateway {
private static final Logger log = LoggerFactory.getLogger(AbstractOpenAiCompatibleGateway.class);
private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
protected final ObjectMapper objectMapper;
protected final OpenAiProtocolMapper openAiProtocolMapper;
protected final ThreadPoolTaskExecutor sseThreadPool;
protected AbstractOpenAiCompatibleGateway(ObjectMapper objectMapper,
OpenAiProtocolMapper openAiProtocolMapper,
ThreadPoolTaskExecutor sseThreadPool) {
this.objectMapper = objectMapper;
this.openAiProtocolMapper = openAiProtocolMapper;
this.sseThreadPool = sseThreadPool;
}
protected Response executePost(Model model, Object requestBody) {
try {
String body = objectMapper.writeValueAsString(requestBody);
Request request = new Request.Builder()
.url(buildUrl(model))
.addHeader("Authorization", "Bearer " + model.checkAndGetApiKey())
.addHeader("Content-Type", "application/json")
.post(RequestBody.create(body, JSON_TYPE))
.build();
return OkHttpClientUtil.buildDefaultClient().newCall(request).execute();
} catch (ModelInvokeException e) {
throw e;
} catch (Exception e) {
throw ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage());
}
}
protected String buildUrl(Model model) {
return StrUtil.removeSuffix(model.checkAndGetEndpoint(), "/")
+ "/"
+ StrUtil.removePrefix(model.checkAndGetRequestPath(), "/");
}
protected void validateResponse(Response response) throws IOException {
if (response.isSuccessful()) {
return;
}
String message = extractUpstreamErrorMessage(response);
int code = response.code();
if (code >= 500 || code == 429) {
throw ModelInvokeException.serviceUnavailable(message);
}
throw ModelInvokeException.badGateway(message);
}
protected String extractUpstreamErrorMessage(Response response) throws IOException {
ResponseBody body = response.body();
String bodyString = body == null ? "" : body.string();
if (StrUtil.isBlank(bodyString)) {
return "上游模型调用失败HTTP " + response.code();
}
try {
JsonNode root = objectMapper.readTree(bodyString);
String errorMessage = root.path("error").path("message").asText();
if (StrUtil.isNotBlank(errorMessage)) {
return "上游模型调用失败HTTP " + response.code() + ": " + errorMessage;
}
} catch (Exception ignored) {
}
return "上游模型调用失败HTTP " + response.code() + ": " + bodyString;
}
protected void streamResponse(Response response,
tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver observer) {
sseThreadPool.execute(() -> {
try (Response closeableResponse = response; ResponseBody body = closeableResponse.body()) {
if (body == null) {
throw new IOException("上游流式响应体为空");
}
BufferedSource source = body.source();
while (!source.exhausted()) {
String line = source.readUtf8Line();
if (line == null) {
break;
}
if (line.isBlank() || !line.startsWith("data:")) {
continue;
}
String payload = line.substring(5).trim();
if (Objects.equals(payload, "[DONE]")) {
observer.onComplete();
return;
}
OpenAiChatCompletionChunkResponse chunkResponse = objectMapper.readValue(
payload,
OpenAiChatCompletionChunkResponse.class
);
UnifiedChatChunk chunk = openAiProtocolMapper.toUnifiedChunk(chunkResponse);
observer.onChunk(chunk);
}
observer.onComplete();
} catch (Exception e) {
log.error("streamResponse error: {}", e.getMessage(), e);
observer.onError(e);
}
});
}
}

View File

@@ -0,0 +1,78 @@
package tech.easyflow.ai.invoke.provider.openai;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.Response;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse;
import tech.easyflow.ai.invoke.provider.ModelProviderGateway;
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
import tech.easyflow.ai.invoke.provider.base.AbstractOpenAiCompatibleGateway;
import java.util.Set;
@Component
public class OpenAiGateway extends AbstractOpenAiCompatibleGateway implements ModelProviderGateway {
private static final Set<String> OPENAI_COMPATIBLE_PROVIDER_TYPES = Set.of(
"openai",
"deepseek",
"aliyun",
"zhipu",
"minimax",
"kimi",
"siliconlow",
"ollama",
"self-hosted"
);
public OpenAiGateway(ObjectMapper objectMapper,
OpenAiProtocolMapper openAiProtocolMapper,
ThreadPoolTaskExecutor sseThreadPool) {
super(objectMapper, openAiProtocolMapper, sseThreadPool);
}
@Override
public boolean supports(String providerType) {
return providerType != null && OPENAI_COMPATIBLE_PROVIDER_TYPES.contains(providerType.toLowerCase());
}
@Override
public UnifiedChatResponse chat(Model model, UnifiedChatRequest request) {
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.toOpenAiRequest(request);
try (Response response = executePost(model, openAiRequest)) {
validateResponse(response);
if (response.body() == null) {
throw tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("上游模型响应为空");
}
OpenAiChatCompletionResponse responseBody = objectMapper.readValue(
response.body().string(),
OpenAiChatCompletionResponse.class
);
return openAiProtocolMapper.toUnifiedResponse(responseBody);
} catch (tech.easyflow.ai.invoke.exception.ModelInvokeException e) {
throw e;
} catch (Exception e) {
throw tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage());
}
}
@Override
public void chatStream(Model model, UnifiedChatRequest request, UnifiedChatChunkObserver observer) {
OpenAiChatCompletionRequest openAiRequest = openAiProtocolMapper.toOpenAiRequest(request);
Response response = executePost(model, openAiRequest);
try {
validateResponse(response);
streamResponse(response, observer);
} catch (RuntimeException | java.io.IOException e) {
response.close();
throw e instanceof RuntimeException ? (RuntimeException) e
: tech.easyflow.ai.invoke.exception.ModelInvokeException.badGateway("调用上游模型失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,12 @@
package tech.easyflow.ai.invoke.service;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
public interface UnifiedModelInvokeService {
UnifiedChatResponse chat(UnifiedChatRequest request);
void chatStream(UnifiedChatRequest request, UnifiedChatChunkObserver observer);
}

View File

@@ -0,0 +1,114 @@
package tech.easyflow.ai.invoke.service.impl;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
import tech.easyflow.ai.invoke.model.UnifiedContentPart;
import tech.easyflow.ai.invoke.model.UnifiedMessage;
import tech.easyflow.ai.invoke.provider.ModelProviderGateway;
import tech.easyflow.ai.invoke.provider.ModelProviderGatewayRegistry;
import tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver;
import tech.easyflow.ai.invoke.service.UnifiedModelInvokeService;
import tech.easyflow.ai.service.ModelService;
import java.util.List;
@Service
public class UnifiedModelInvokeServiceImpl implements UnifiedModelInvokeService {
private final ModelService modelService;
private final ModelProviderGatewayRegistry gatewayRegistry;
public UnifiedModelInvokeServiceImpl(ModelService modelService,
ModelProviderGatewayRegistry gatewayRegistry) {
this.modelService = modelService;
this.gatewayRegistry = gatewayRegistry;
}
@Override
public UnifiedChatResponse chat(UnifiedChatRequest request) {
Model model = resolveModel(request);
validateRequestAgainstModel(model, request);
ModelProviderGateway gateway = gatewayRegistry.getGateway(model.getModelProvider().getProviderType());
return gateway.chat(model, request);
}
@Override
public void chatStream(UnifiedChatRequest request, UnifiedChatChunkObserver observer) {
Model model = resolveModel(request);
validateRequestAgainstModel(model, request);
ModelProviderGateway gateway = gatewayRegistry.getGateway(model.getModelProvider().getProviderType());
gateway.chatStream(model, request, observer);
}
private Model resolveModel(UnifiedChatRequest request) {
if (request == null || StrUtil.isBlank(request.getModel())) {
throw ModelInvokeException.badRequest("model 不能为空", "model", "model_required");
}
Model model = modelService.getModelInstanceByInvokeCode(request.getModel());
if (model == null) {
throw ModelInvokeException.notFound("未找到可调用模型: " + request.getModel());
}
if (!Boolean.TRUE.equals(model.getPublishEnabled())) {
throw ModelInvokeException.badRequest("当前模型未开启 API 调用发布", "model", "model_not_published");
}
if (!Model.MODEL_TYPES[0].equals(model.getModelType())) {
throw ModelInvokeException.badRequest("当前模型不是 chatModel无法通过 chat/completions 调用", "model", "model_type_mismatch");
}
if (model.getModelProvider() == null || StrUtil.isBlank(model.getModelProvider().getProviderType())) {
throw ModelInvokeException.badRequest("当前模型缺少 providerType 配置", "model", "provider_type_missing");
}
return model;
}
private void validateRequestAgainstModel(Model model, UnifiedChatRequest request) {
List<UnifiedMessage> messages = request.getMessages();
if (messages == null || messages.isEmpty()) {
throw ModelInvokeException.badRequest("messages 不能为空", "messages", "messages_required");
}
if (hasImageInput(messages)) {
if (!Boolean.TRUE.equals(model.getSupportImage())) {
throw ModelInvokeException.badRequest("当前模型不支持图片输入", "messages", "image_not_supported");
}
if (Boolean.TRUE.equals(model.getSupportImageB64Only()) && hasNonBase64Image(messages)) {
throw ModelInvokeException.badRequest("当前模型仅支持 base64 图片输入", "messages", "image_base64_only");
}
}
if (request.getTools() != null && !request.getTools().isEmpty() && !Boolean.TRUE.equals(model.getSupportTool())) {
throw ModelInvokeException.badRequest("当前模型不支持 tools 参数", "tools", "tool_not_supported");
}
if (hasToolMessage(messages) && !Boolean.TRUE.equals(model.getSupportToolMessage())) {
throw ModelInvokeException.badRequest("当前模型不支持 tool 消息透传", "messages", "tool_message_not_supported");
}
}
private boolean hasImageInput(List<UnifiedMessage> messages) {
return messages.stream()
.map(UnifiedMessage::getContentParts)
.filter(parts -> parts != null && !parts.isEmpty())
.flatMap(List::stream)
.anyMatch(part -> "image_url".equals(part.getType()));
}
private boolean hasNonBase64Image(List<UnifiedMessage> messages) {
return messages.stream()
.map(UnifiedMessage::getContentParts)
.filter(parts -> parts != null && !parts.isEmpty())
.flatMap(List::stream)
.filter(part -> "image_url".equals(part.getType()))
.map(UnifiedContentPart::getImageUrl)
.filter(imageUrl -> imageUrl != null && StrUtil.isNotBlank(imageUrl.getUrl()))
.anyMatch(imageUrl -> !StrUtil.startWithIgnoreCase(imageUrl.getUrl(), "data:"));
}
private boolean hasToolMessage(List<UnifiedMessage> messages) {
return messages.stream().anyMatch(message ->
StrUtil.equals(message.getRole(), "tool")
|| (message.getToolCalls() != null && !message.getToolCalls().isEmpty())
|| StrUtil.isNotBlank(message.getToolCallId())
);
}
}

View File

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

View File

@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.ModelProvider;
@@ -182,6 +183,125 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
throw new BusinessException("模型ID不能为空");
}
Model model = modelMapper.selectOneWithRelationsById(modelId);
return fillProviderDefaults(model);
}
@Override
public Model getModelInstanceByInvokeCode(String invokeCode) {
if (StrUtil.isBlank(invokeCode)) {
throw new BusinessException("invokeCode不能为空");
}
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getInvokeCode, invokeCode.trim());
Model model = modelMapper.selectOneWithRelationsByQuery(queryWrapper);
return fillProviderDefaults(model);
}
@Override
public void validateForSaveOrUpdate(Model entity, boolean isSave) {
if (entity == null) {
throw new BusinessException("模型配置不能为空");
}
if (entity.getPublishEnabled() == null) {
entity.setPublishEnabled(Boolean.FALSE);
}
String originalInvokeCode = StrUtil.trim(entity.getInvokeCode());
String invokeCode = originalInvokeCode;
boolean autoGeneratedInvokeCode = StrUtil.isBlank(invokeCode);
if (autoGeneratedInvokeCode) {
invokeCode = buildDefaultInvokeCode(entity.getModelName());
}
if (Boolean.TRUE.equals(entity.getPublishEnabled())) {
if (StrUtil.isBlank(invokeCode)) {
throw new BusinessException("开启 API 调用前,请先配置 invokeCode");
}
if (!Model.MODEL_TYPES[0].equals(entity.getModelType())) {
throw new BusinessException("只有聊天模型支持开启 API 调用");
}
}
if (StrUtil.isBlank(invokeCode)) {
entity.setInvokeCode(null);
return;
}
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getInvokeCode, invokeCode);
if (!isSave && entity.getId() != null) {
queryWrapper.ne(Model::getId, entity.getId());
}
boolean duplicated = modelMapper.selectCountByQuery(queryWrapper) > 0;
if (duplicated && autoGeneratedInvokeCode && !Boolean.TRUE.equals(entity.getPublishEnabled())) {
entity.setInvokeCode(null);
return;
}
if (duplicated) {
throw new BusinessException("invokeCode 已存在,请更换后重试");
}
entity.setInvokeCode(invokeCode);
}
@Override
public List<Model> listInvokeModels() {
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getModelType, Model.MODEL_TYPES[0]);
return modelMapper.selectListWithRelationsByQuery(queryWrapper).stream()
.map(this::fillProviderDefaults)
.collect(Collectors.toList());
}
@Override
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
Model existing = getModelInstance(id);
if (existing == null) {
throw new BusinessException("模型不存在");
}
if (!Model.MODEL_TYPES[0].equals(existing.getModelType())) {
throw new BusinessException("只有聊天模型支持统一网关配置");
}
existing.setInvokeCode(invokeCode);
existing.setPublishEnabled(Boolean.TRUE.equals(publishEnabled));
validateForSaveOrUpdate(existing, false);
modelMapper.update(existing);
return getModelInstance(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled) {
if (CollectionUtils.isEmpty(ids)) {
throw new BusinessException("请选择要操作的模型");
}
List<BigInteger> uniqueIds = new ArrayList<>(new LinkedHashSet<>(ids));
List<Model> updatedModels = new ArrayList<>(uniqueIds.size());
for (BigInteger id : uniqueIds) {
updatedModels.add(updateInvokeConfig(id, null, publishEnabled));
}
return updatedModels;
}
private String buildDefaultInvokeCode(String modelName) {
if (StrUtil.isBlank(modelName)) {
return null;
}
String normalized = modelName.trim()
.replaceAll("[^A-Za-z0-9._:-]+", "-")
.replaceAll("-{2,}", "-")
.replaceFirst("^[^A-Za-z0-9]+", "");
if (StrUtil.isBlank(normalized)) {
return null;
}
if (normalized.length() > 128) {
normalized = normalized.substring(0, 128);
}
if (normalized.length() == 1) {
normalized = normalized + "-model";
}
return normalized;
}
private Model fillProviderDefaults(Model model) {
if (model == null) {
return null;
}

View File

@@ -0,0 +1,192 @@
package tech.easyflow.ai.invoke.mapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.invoke.exception.ModelInvokeException;
import tech.easyflow.ai.invoke.model.UnifiedChatRequest;
import tech.easyflow.ai.invoke.model.UnifiedChatResponse;
import tech.easyflow.ai.invoke.model.UnifiedChoice;
import tech.easyflow.ai.invoke.model.UnifiedContentPart;
import tech.easyflow.ai.invoke.model.UnifiedImageUrl;
import tech.easyflow.ai.invoke.model.UnifiedMessage;
import tech.easyflow.ai.invoke.model.UnifiedResponseFormat;
import tech.easyflow.ai.invoke.model.UnifiedTool;
import tech.easyflow.ai.invoke.model.UnifiedToolCall;
import tech.easyflow.ai.invoke.model.UnifiedToolCallFunction;
import tech.easyflow.ai.invoke.model.UnifiedToolFunction;
import tech.easyflow.ai.invoke.model.UnifiedUsage;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionRequest;
import tech.easyflow.ai.invoke.protocol.openai.OpenAiChatCompletionResponse;
import java.util.Collections;
import java.util.List;
public class OpenAiProtocolMapperTest {
private final OpenAiProtocolMapper mapper = new OpenAiProtocolMapper(new ObjectMapper());
@Test
public void shouldParseTextAndImageRequest() {
String rawBody = """
{
"model": "gpt-4-1-prod",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "帮我看下这张图"
},
{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,AAAA",
"detail": "high"
}
}
]
}
],
"stream": false,
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 512,
"seed": 7,
"tools": [
{
"type": "function",
"function": {
"name": "query_weather",
"description": "query weather",
"parameters": {
"type": "object"
}
}
}
],
"tool_choice": "auto",
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "weather_schema"
}
}
}
""";
OpenAiChatCompletionRequest request = mapper.readRequest(rawBody);
UnifiedChatRequest unifiedRequest = mapper.toUnifiedRequest(request);
Assert.assertEquals("gpt-4-1-prod", unifiedRequest.getModel());
Assert.assertEquals(Long.valueOf(7), unifiedRequest.getSeed());
Assert.assertEquals(Integer.valueOf(512), unifiedRequest.getMaxTokens());
Assert.assertNotNull(unifiedRequest.getTools());
Assert.assertEquals(1, unifiedRequest.getTools().size());
Assert.assertEquals("query_weather", unifiedRequest.getTools().get(0).getFunction().getName());
Assert.assertEquals("json_schema", unifiedRequest.getResponseFormat().getType());
Assert.assertEquals("weather_schema", unifiedRequest.getResponseFormat().getJsonSchema().get("name").asText());
UnifiedMessage message = unifiedRequest.getMessages().get(0);
Assert.assertEquals("user", message.getRole());
Assert.assertNotNull(message.getContentParts());
Assert.assertEquals(2, message.getContentParts().size());
UnifiedContentPart imagePart = message.getContentParts().get(1);
Assert.assertEquals("image_url", imagePart.getType());
Assert.assertEquals("data:image/png;base64,AAAA", imagePart.getImageUrl().getUrl());
Assert.assertEquals("high", imagePart.getImageUrl().getDetail());
}
@Test
public void shouldRejectUnsupportedRootField() {
String rawBody = """
{
"model": "gpt-4-1-prod",
"messages": [
{
"role": "user",
"content": "hello"
}
],
"n": 2
}
""";
ModelInvokeException exception = Assert.assertThrows(
ModelInvokeException.class,
() -> mapper.readRequest(rawBody)
);
Assert.assertEquals(400, exception.getStatus());
Assert.assertEquals("unsupported_field", exception.getCode());
}
@Test
public void shouldAllowMissingOptionalFields() {
String rawBody = """
{
"model": "deepseek-chat",
"messages": [
{
"role": "user",
"content": "你好,介绍一下你自己。"
}
]
}
""";
OpenAiChatCompletionRequest request = mapper.readRequest(rawBody);
UnifiedChatRequest unifiedRequest = mapper.toUnifiedRequest(request);
Assert.assertEquals("deepseek-chat", unifiedRequest.getModel());
Assert.assertNotNull(unifiedRequest.getMessages());
Assert.assertEquals(1, unifiedRequest.getMessages().size());
Assert.assertNull(unifiedRequest.getTools());
Assert.assertNull(unifiedRequest.getToolChoice());
Assert.assertNull(unifiedRequest.getResponseFormat());
}
@Test
public void shouldMapToolCallsAndUsageInResponse() {
UnifiedChatResponse response = new UnifiedChatResponse();
response.setId("chatcmpl-1");
response.setObject("chat.completion");
response.setCreated(123L);
response.setModel("gpt-4-1-prod");
UnifiedToolCallFunction toolCallFunction = new UnifiedToolCallFunction();
toolCallFunction.setName("query_weather");
toolCallFunction.setArguments("{\"city\":\"shanghai\"}");
UnifiedToolCall toolCall = new UnifiedToolCall();
toolCall.setId("call_1");
toolCall.setType("function");
toolCall.setFunction(toolCallFunction);
UnifiedMessage message = new UnifiedMessage();
message.setRole("assistant");
message.setContent(null);
message.setToolCalls(Collections.singletonList(toolCall));
UnifiedChoice choice = new UnifiedChoice();
choice.setIndex(0);
choice.setMessage(message);
choice.setFinishReason("tool_calls");
UnifiedUsage usage = new UnifiedUsage();
usage.setPromptTokens(12);
usage.setCompletionTokens(34);
usage.setTotalTokens(46);
response.setChoices(List.of(choice));
response.setUsage(usage);
OpenAiChatCompletionResponse openAiResponse = mapper.toOpenAiResponse(response);
Assert.assertEquals("chatcmpl-1", openAiResponse.getId());
Assert.assertEquals("tool_calls", openAiResponse.getChoices().get(0).getFinishReason());
Assert.assertEquals("query_weather", openAiResponse.getChoices().get(0).getMessage().getToolCalls().get(0).getFunction().getName());
Assert.assertEquals(Integer.valueOf(46), openAiResponse.getUsage().getTotalTokens());
}
}

View File

@@ -14,6 +14,7 @@ import tech.easyflow.system.service.SysApiKeyService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -34,21 +35,36 @@ public class SysApiKeyServiceImpl extends ServiceImpl<SysApiKeyMapper, SysApiKey
@Override
public void checkApikeyPermission(String apiKey, String requestURI) {
SysApiKey sysApiKey = getSysApiKey(apiKey);
List<String> candidateRequestUris = getCandidateRequestUris(requestURI);
QueryWrapper w = QueryWrapper.create();
w.eq(SysApiKeyResource::getRequestInterface, requestURI);
SysApiKeyResource resource = resourceService.getOne(w);
if (resource == null) {
w.in(SysApiKeyResource::getRequestInterface, candidateRequestUris);
List<SysApiKeyResource> resources = resourceService.list(w);
if (resources == null || resources.isEmpty()) {
throw new BusinessException("该接口不存在");
}
List<BigInteger> resourceIds = resources.stream()
.map(SysApiKeyResource::getId)
.toList();
QueryWrapper wm = QueryWrapper.create();
wm.eq(SysApiKeyResourceMapping::getApiKeyId, sysApiKey.getId());
wm.eq(SysApiKeyResourceMapping::getApiKeyResourceId, resource.getId());
wm.in(SysApiKeyResourceMapping::getApiKeyResourceId, resourceIds);
long count = mappingService.count(wm);
if (count == 0) {
throw new BusinessException("该apiKey无权限访问该接口");
}
}
private List<String> getCandidateRequestUris(String requestURI) {
List<String> uris = new ArrayList<>();
uris.add(requestURI);
if ("/v1/chat/completions".equals(requestURI)) {
uris.add("/public-api/openai/v1/chat/completions");
} else if ("/public-api/openai/v1/chat/completions".equals(requestURI)) {
uris.add("/v1/chat/completions");
}
return uris;
}
@Override
public SysApiKey getSysApiKey(String apiKey) {
QueryWrapper w = QueryWrapper.create();

View File

@@ -186,34 +186,6 @@ INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`,
INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000003, 'Kimi', 'kimi', '', '', 'https://api.moonshot.cn', '/v1/chat/completions', '', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1);
INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000004, '自部署', 'self-hosted', '', '', 'http://127.0.0.1:8000', '/v1/chat/completions', '/v1/embeddings', '/v1/score', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1);
-- ----------------------------
-- Records of tb_model
-- ----------------------------
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010001, 1, 1000000, 359111120310632448, 'DeepSeek-V3', NULL, '通用对话与代码任务表现均衡。', NULL, NULL, 'deepseek-chat', NULL, NULL, '{"llmEndpoint":"https://api.deepseek.com","chatPath":"/chat/completions","embedPath":"","rerankPath":""}', 'DeepSeek', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010002, 1, 1000000, 359111120310632448, 'DeepSeek-R1', NULL, '复杂推理与长链路分析场景。', NULL, NULL, 'deepseek-reasoner', NULL, NULL, '{"llmEndpoint":"https://api.deepseek.com","chatPath":"/chat/completions","embedPath":"","rerankPath":""}', 'DeepSeek', 'chatModel', 1, 1, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010003, 1, 1000000, 359111228158771200, 'o4-mini', NULL, '轻量推理与通用生产任务模型。', NULL, NULL, 'o4-mini', NULL, NULL, '{"llmEndpoint":"https://api.openai.com","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":""}', 'OpenAI', 'chatModel', 1, 1, 1, 1, 0, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010004, 1, 1000000, 359111228158771200, 'GPT-4.1', NULL, '复杂任务与多模态理解旗舰模型。', NULL, NULL, 'gpt-4.1', NULL, NULL, '{"llmEndpoint":"https://api.openai.com","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":""}', 'OpenAI', 'chatModel', 1, 1, 1, 1, 0, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010005, 1, 1000000, 359111228158771200, 'text-embedding-3-large', NULL, '高质量文本向量模型。', NULL, NULL, 'text-embedding-3-large', NULL, NULL, '{"llmEndpoint":"https://api.openai.com","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":""}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010006, 1, 1000000, 359111448204541952, '通义千问 Plus', NULL, '适合通用问答与工具调用。', NULL, NULL, 'qwen-plus', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Qwen', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010007, 1, 1000000, 359111448204541952, '通义千问 Max', NULL, '综合能力更强的主力模型。', NULL, NULL, 'qwen-max', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Qwen', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010008, 1, 1000000, 359111448204541952, 'text-embedding-v4', NULL, '阿里百炼默认向量模型。', NULL, NULL, 'text-embedding-v4', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010009, 1, 1000000, 359111448204541952, 'gte-rerank-v2', NULL, '阿里百炼默认重排模型。', NULL, NULL, 'gte-rerank-v2', NULL, NULL, '{"llmEndpoint":"https://dashscope.aliyuncs.com","chatPath":"/compatible-mode/v1/chat/completions","embedPath":"/compatible-mode/v1/embeddings","rerankPath":"/api/v1/services/rerank/text-rerank/text-rerank"}', 'Rerank', 'rerankModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010010, 1, 1000000, 366100000000000001, 'GLM-4.5', NULL, '中文与推理能力表现均衡。', NULL, NULL, 'glm-4.5', NULL, NULL, '{"llmEndpoint":"https://open.bigmodel.cn","chatPath":"/api/paas/v4/chat/completions","embedPath":"/api/paas/v4/embeddings","rerankPath":""}', 'GLM', 'chatModel', 1, 1, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010011, 1, 1000000, 366100000000000001, 'GLM-4.5-Air', NULL, '低延迟通用模型。', NULL, NULL, 'glm-4.5-air', NULL, NULL, '{"llmEndpoint":"https://open.bigmodel.cn","chatPath":"/api/paas/v4/chat/completions","embedPath":"/api/paas/v4/embeddings","rerankPath":""}', 'GLM', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010012, 1, 1000000, 366100000000000001, 'Embedding-3', NULL, '知识库与检索常用向量模型。', NULL, NULL, 'embedding-3', NULL, NULL, '{"llmEndpoint":"https://open.bigmodel.cn","chatPath":"/api/paas/v4/chat/completions","embedPath":"/api/paas/v4/embeddings","rerankPath":""}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010013, 1, 1000000, 366100000000000002, 'MiniMax-M2.5', NULL, '长上下文与复杂推理场景。', NULL, NULL, 'MiniMax-M2.5', NULL, NULL, '{"llmEndpoint":"https://api.minimax.io","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'MiniMax', 'chatModel', 1, 1, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010014, 1, 1000000, 366100000000000002, 'MiniMax-M2.5-highspeed', NULL, '更高吞吐的快速对话模型。', NULL, NULL, 'MiniMax-M2.5-highspeed', NULL, NULL, '{"llmEndpoint":"https://api.minimax.io","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'MiniMax', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010015, 1, 1000000, 366100000000000003, 'moonshot-v1-8k', NULL, '低延迟通用对话模型。', NULL, NULL, 'moonshot-v1-8k', NULL, NULL, '{"llmEndpoint":"https://api.moonshot.cn","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'Kimi', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010016, 1, 1000000, 366100000000000003, 'moonshot-v1-128k', NULL, '长文档理解与大上下文任务。', NULL, NULL, 'moonshot-v1-128k', NULL, NULL, '{"llmEndpoint":"https://api.moonshot.cn","chatPath":"/v1/chat/completions","embedPath":"","rerankPath":""}', 'Kimi', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010017, 1, 1000000, 359110667376132096, 'DeepSeek-V3', NULL, '统一平台接入的开源对话模型。', NULL, NULL, 'deepseek-ai/DeepSeek-V3', NULL, NULL, '{"llmEndpoint":"https://api.siliconflow.cn","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/rerank"}', 'DeepSeek', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010018, 1, 1000000, 359110667376132096, 'Kimi-K2-Instruct', NULL, '代码与 Agent 工作流场景常用模型。', NULL, NULL, 'moonshotai/Kimi-K2-Instruct', NULL, NULL, '{"llmEndpoint":"https://api.siliconflow.cn","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/rerank"}', 'Kimi', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010019, 1, 1000000, 359110667376132096, 'BAAI/bge-m3', NULL, '多语言检索与向量召回模型。', NULL, NULL, 'BAAI/bge-m3', NULL, NULL, '{"llmEndpoint":"https://api.siliconflow.cn","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/rerank"}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010020, 1, 1000000, 359110690079899648, 'qwen3:8b', NULL, '本地开发常用的通用对话模型。', NULL, NULL, 'qwen3:8b', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:11434","chatPath":"/v1/chat/completions","embedPath":"/api/embed","rerankPath":""}', 'Ollama', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010021, 1, 1000000, 359110690079899648, 'bge-m3', NULL, '本地向量化与检索验证。', NULL, NULL, 'bge-m3', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:11434","chatPath":"/v1/chat/completions","embedPath":"/api/embed","rerankPath":""}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010022, 1, 1000000, 366100000000000004, 'Qwen/Qwen3-32B', NULL, '适合自部署对话与工具调用。', NULL, NULL, 'Qwen/Qwen3-32B', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:8000","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/score"}', 'Qwen', 'chatModel', 1, 0, 1, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010023, 1, 1000000, 366100000000000004, 'BAAI/bge-m3', NULL, '自部署检索和知识库向量化。', NULL, NULL, 'BAAI/bge-m3', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:8000","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/score"}', 'Embedding', 'embeddingModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
INSERT INTO `tb_model` (`id`, `dept_id`, `tenant_id`, `provider_id`, `title`, `icon`, `description`, `endpoint`, `request_path`, `model_name`, `api_key`, `extra_config`, `options`, `group_name`, `model_type`, `with_used`, `support_thinking`, `support_tool`, `support_image`, `support_image_b64_only`, `support_video`, `support_audio`, `support_free`) VALUES (366100000000010024, 1, 1000000, 366100000000000004, 'jinaai/jina-reranker-m0', NULL, '自部署召回重排模型。', NULL, NULL, 'jinaai/jina-reranker-m0', NULL, NULL, '{"llmEndpoint":"http://127.0.0.1:8000","chatPath":"/v1/chat/completions","embedPath":"/v1/embeddings","rerankPath":"/v1/score"}', 'Rerank', 'rerankModel', 1, 0, 0, NULL, NULL, NULL, NULL, 0);
-- ----------------------------
-- Records of tb_sys_option
-- ----------------------------

View File

@@ -0,0 +1,18 @@
SET NAMES utf8mb4;
ALTER TABLE `tb_model`
ADD COLUMN `invoke_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '统一模型调用对外标识' AFTER `support_tool_message`,
ADD COLUMN `publish_enabled` tinyint NULL DEFAULT 0 COMMENT '是否开启统一模型调用发布' AFTER `invoke_code`;
CREATE UNIQUE INDEX `uni_model_invoke_code` ON `tb_model` (`invoke_code`);
INSERT INTO `tb_sys_api_key_resource` (`id`, `request_interface`, `title`)
SELECT 366700000000000001,
'/v1/chat/completions',
'统一模型调用OpenAI Chat Completions'
FROM DUAL
WHERE NOT EXISTS(
SELECT 1
FROM `tb_sys_api_key_resource`
WHERE `request_interface` = '/v1/chat/completions'
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>BaiLian</title><path d="M6.336 8.919v6.162l5.335-3.083L6.337 8.92z" fill="#1C54E3"></path><path d="M21.394 5.288s-.006-.006-.01-.006L17.01 2.754 6.336 8.92l5.335 3.082 9.701-5.6.016-.01a.635.635 0 00.006-1.1v-.003z" fill="#AA9AFF"></path><path d="M21.71 12.465a.62.62 0 00-.316.085s-.006 0-.009.003l-4.375 2.528 5.05 2.915h.006a2.06 2.06 0 00.28-1.04v-3.855a.637.637 0 00-.636-.636z" fill="#00EAD1"></path><path d="M22.06 17.996l-5.05-2.915L6.34 21.242l4.27 2.465s.016.006.022.012a2.102 2.102 0 002.093 0c.006-.003.016-.006.022-.012l8.538-4.93c.003 0 .006-.003.01-.006.321-.183.589-.45.775-.772h-.006l-.004-.003z" fill="#00CEC9"></path><path d="M11.672 11.998l-5.336 3.083-1.444.832-3.605 2.083H1.28c.173.303.416.555.709.738l.078.044.016.01.02.012 4.232 2.442 10.671-6.161-5.335-3.082z" fill="#00EAD1"></path><path d="M12.74.29c-.1-.06-.208-.107-.315-.148-.02-.006-.038-.016-.057-.022a2.121 2.121 0 00-.7-.12c-.233 0-.457.038-.668.11l-.031.01a2.196 2.196 0 00-.372.17L2.068 5.222s-.003 0-.006.003c-.324.183-.592.451-.781.773h.006l5.049 2.918L17.01 2.758 12.74.29z" fill="#7347FF"></path><path d="M1.287 6.001H1.28A2.06 2.06 0 001 7.041v9.915c0 .378.1.735.28 1.043h.007l5.049-2.918V8.919l-5.05-2.918z" fill="#0423DA"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,2 @@
429: Too Many Requests
For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service).

View File

@@ -0,0 +1,2 @@
429: Too Many Requests
For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service).

View File

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>MoonshotAI</title><path d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z"></path></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><defs><linearGradient id="lobe-icons-minimax-fill" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"></stop><stop offset="100%" stop-color="#FE603C"></stop></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-fill)" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Ollama</title><path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z" fill="url(#lobe-icons-qwen-fill)" fill-rule="nonzero"></path><defs><linearGradient id="lobe-icons-qwen-fill" x1="0%" x2="100%" y1="0%" y2="0%"><stop offset="0%" stop-color="#6336E7" stop-opacity=".84"></stop><stop offset="100%" stop-color="#6F69F7" stop-opacity=".84"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>vLLM</title><path d="M0 4.973h9.324V23L0 4.973z" fill="#FDB515"></path><path d="M13.986 4.351L22.378 0l-6.216 23H9.324l4.662-18.649z" fill="#30A2FF"></path></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Zhipu</title><path d="M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z" fill="#3859FF" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="146 227 268 266"
fill="none"
>
<path
d="M249.176 323.434V298.276C249.176 296.158 249.971 294.569 251.825 293.509L302.406 264.381C309.29 260.409 317.5 258.555 325.973 258.555C357.75 258.555 377.877 283.185 377.877 309.399C377.877 311.253 377.877 313.371 377.611 315.49L325.178 284.771C322.001 282.919 318.822 282.919 315.645 284.771L249.176 323.434ZM367.283 421.415V361.301C367.283 357.592 365.694 354.945 362.516 353.092L296.048 314.43L317.763 301.982C319.617 300.925 321.206 300.925 323.058 301.982L373.639 331.112C388.205 339.586 398.003 357.592 398.003 375.069C398.003 395.195 386.087 413.733 367.283 421.412V421.415ZM233.553 368.452L211.838 355.742C209.986 354.684 209.19 353.095 209.19 350.975V292.718C209.19 264.383 230.905 242.932 260.301 242.932C271.423 242.932 281.748 246.641 290.49 253.26L238.321 283.449C235.146 285.303 233.555 287.951 233.555 291.659V368.455L233.553 368.452ZM280.292 395.462L249.176 377.985V340.913L280.292 323.436L311.407 340.913V377.985L280.292 395.462ZM300.286 475.968C289.163 475.968 278.837 472.259 270.097 465.64L322.264 435.449C325.441 433.597 327.03 430.949 327.03 427.239V350.445L349.011 363.155C350.865 364.213 351.66 365.802 351.66 367.922V426.179C351.66 454.514 329.679 475.965 300.286 475.965V475.968ZM237.525 416.915L186.944 387.785C172.378 379.31 162.582 361.305 162.582 343.827C162.582 323.436 174.763 305.164 193.563 297.485V357.861C193.563 361.571 195.154 364.217 198.33 366.071L264.535 404.467L242.82 416.915C240.967 417.972 239.377 417.972 237.525 416.915ZM234.614 460.343C204.689 460.343 182.71 437.833 182.71 410.028C182.71 407.91 182.976 405.792 183.238 403.672L235.405 433.863C238.582 435.715 241.763 435.715 244.938 433.863L311.407 395.466V420.622C311.407 422.742 310.612 424.331 308.758 425.389L258.179 454.519C251.293 458.491 243.083 460.343 234.611 460.343H234.614ZM300.286 491.854C332.329 491.854 359.073 469.082 365.167 438.892C394.825 431.211 413.892 403.406 413.892 375.073C413.892 356.535 405.948 338.529 391.648 325.552C392.972 319.991 393.766 314.43 393.766 308.87C393.766 271.003 363.048 242.666 327.562 242.666C320.413 242.666 313.528 243.723 306.644 246.109C294.725 234.457 278.307 227.042 260.301 227.042C228.258 227.042 201.513 249.815 195.42 280.004C165.761 287.685 146.694 315.49 146.694 343.824C146.694 362.362 154.638 380.368 168.938 393.344C167.613 398.906 166.819 404.467 166.819 410.027C166.819 447.894 197.538 476.231 233.024 476.231C240.172 476.231 247.058 475.173 253.943 472.788C265.859 484.441 282.278 491.854 300.286 491.854Z"
fill="#111827"
/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,8 +0,0 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="28" fill="#475569"/>
<path d="M26 35C26 31.6863 28.6863 29 32 29H64C67.3137 29 70 31.6863 70 35V42C70 45.3137 67.3137 48 64 48H32C28.6863 48 26 45.3137 26 42V35Z" fill="white" fill-opacity="0.95"/>
<path d="M30 40H66" stroke="#475569" stroke-width="4" stroke-linecap="round"/>
<path d="M37 58C37 55.7909 38.7909 54 41 54H55C57.2091 54 59 55.7909 59 58V63C59 65.2091 57.2091 67 55 67H41C38.7909 67 37 65.2091 37 63V58Z" fill="white" fill-opacity="0.9"/>
<path d="M48 48V54" stroke="white" stroke-width="4" stroke-linecap="round"/>
<circle cx="48" cy="60.5" r="2.5" fill="#475569"/>
</svg>

Before

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,4 +1,4 @@
import {api} from '#/api/request.js';
import { api } from '#/api/request.js';
// 获取LLM供应商
export async function getLlmProviderList() {
@@ -14,6 +14,10 @@ export async function getModelList(params: ModelListQuery = {}) {
return api.get('/api/v1/model/list', { params });
}
export async function getInvokeModelList() {
return api.get('/api/v1/model/invokeList');
}
// 保存LLM
export async function saveLlm(data: string) {
return api.post('/api/v1/model/save', data);
@@ -33,6 +37,27 @@ export async function verifyModelConfig(id: string) {
return api.get('/api/v1/model/verifyLlmConfig', { params: { id } });
}
export interface ModelInvokeConfigPayload {
id: string;
invokeCode?: string;
publishEnabled: boolean;
}
export async function updateModelInvokeConfig(data: ModelInvokeConfigPayload) {
return api.post('/api/v1/model/updateInvokeConfig', data);
}
export interface BatchModelInvokePublishPayload {
ids: string[];
publishEnabled: boolean;
}
export async function batchUpdateInvokePublishStatus(
data: BatchModelInvokePublishPayload,
) {
return api.post('/api/v1/model/batchUpdateInvokePublishStatus', data);
}
// 一键添加LLM
export async function quickAddLlm(data: any) {
return api.post(`/api/v1/model/quickAdd`, data);
@@ -40,7 +65,9 @@ export async function quickAddLlm(data: any) {
export interface llmType {
id: string;
providerId?: string;
title: string;
modelName?: string;
modelProvider: {
icon: string;
providerName: string;
@@ -51,6 +78,12 @@ export interface llmType {
description: string;
modelType: string;
groupName: string;
invokeCode?: string;
publishEnabled?: boolean;
supportTool?: boolean;
supportImage?: boolean;
supportImageB64Only?: boolean;
supportToolMessage?: boolean;
added: boolean;
aiLlmProvider: any;
}

View File

@@ -1,45 +1,43 @@
{
"filed":
{
"id": "Id",
"deptId": "DeptId",
"tenantId": "TenantId",
"title": "Title",
"brand": "Brand",
"icon": "Icon",
"description": "Description",
"supportChat": "SupportChat",
"supportFunctionCalling": "SupportFunctionCalling",
"supportEmbed": "SupportEmbed",
"supportReranker": "SupportReranker",
"supportTextToImage": "SupportTextToImage",
"supportImageToImage": "SupportImageToImage",
"supportTextToAudio": "SupportTextToAudio",
"supportAudioToAudio": "SupportAudioToAudio",
"supportTextToVideo": "SupportTextToVideo",
"supportImageToVideo": "SupportImageToVideo",
"multimodal": "multimodal",
"llmEndpoint": "LlmEndpoint",
"chatPath": "ChatPath",
"embedPath": "embedPath",
"llmModel": "LlmModel",
"llmApiKey": "apiKey",
"llmExtraConfig": "LlmExtraConfig",
"options": "Options",
"ability": "Ability"
}
,
"filed": {
"id": "Id",
"deptId": "DeptId",
"tenantId": "TenantId",
"title": "Title",
"brand": "Brand",
"icon": "Icon",
"description": "Description",
"supportChat": "SupportChat",
"supportFunctionCalling": "SupportFunctionCalling",
"supportEmbed": "SupportEmbed",
"supportReranker": "SupportReranker",
"supportTextToImage": "SupportTextToImage",
"supportImageToImage": "SupportImageToImage",
"supportTextToAudio": "SupportTextToAudio",
"supportAudioToAudio": "SupportAudioToAudio",
"supportTextToVideo": "SupportTextToVideo",
"supportImageToVideo": "SupportImageToVideo",
"multimodal": "multimodal",
"llmEndpoint": "LlmEndpoint",
"chatPath": "ChatPath",
"embedPath": "embedPath",
"llmModel": "Model ID",
"llmApiKey": "apiKey",
"llmExtraConfig": "LlmExtraConfig",
"options": "Options",
"ability": "Ability"
},
"llmModal": {
"TitleRequired": "Please enter the name",
"BrandRequired": "Please enter the brand",
"ModelRequired": "Please enter the model",
"ModelRequired": "Please enter the model ID",
"ApiKeyRequired": "Please enter the apiKey",
"QuickAddLlm": "One-click addition of large models"
},
"placeholder": {
"title": "Please enter the title",
"brand": "Please enter the brand",
"llmModel": "Please enter the llmModel",
"llmModel": "Please enter the model ID",
"description": "Please enter the description"
},
"actions": {
@@ -50,7 +48,7 @@
},
"addProvider": "Provider list",
"modelType": "ModelType",
"llmModel": "ModelName",
"llmModel": "Model ID",
"title": "name",
"groupName": "GroupName",
"provider": "供应商",
@@ -62,7 +60,7 @@
},
"all": "All",
"verifyLlmTitle": "Verify Large Model",
"searchTextPlaceholder": "Search for model name or name",
"searchTextPlaceholder": "Search for model ID or name",
"testSuccess": "TestSuccess",
"modelAbility": {
"supportThinking": "Thinking",

View File

@@ -2,7 +2,7 @@
"title": "Large Model Configuration",
"modelOfChat": "Chat Model Provider",
"dialogModel": "Chat Model Settings",
"modelName": "Model Name",
"modelName": "Model ID",
"basic": "BasicInformation",
"updatePwd": "UpdatePassword",
"systemAIFunctionSettings": "System AI Function Settings",

View File

@@ -1,9 +1,8 @@
{
"filed":
{
"filed": {
"title": "标题",
"brand": "供应商",
"llmModel": "模型名称",
"llmModel": "模型 ID",
"icon": "ICON",
"description": "描述",
"supportChat": "对话模型",
@@ -24,19 +23,18 @@
"llmExtraConfig": "大模型其他属性配置",
"options": "其他配置内容",
"ability": "能力"
}
,
},
"llmModal": {
"TitleRequired": "请输入名称",
"BrandRequired": "请选择供应商",
"ModelRequired": "请输入模型",
"ModelRequired": "请输入模型 ID",
"ApiKeyRequired": "请输入apiKey",
"QuickAddLlm": "一键添加大模型"
},
"placeholder": {
"title": "请输入名称",
"brand": "请选择品牌",
"llmModel": "请输入模型名称",
"llmModel": "请输入模型 ID",
"description": "请输入描述"
},
"actions": {
@@ -47,7 +45,7 @@
},
"addProvider": "供应商列表",
"modelType": "模型类型",
"llmModel": "模型名称",
"llmModel": "模型 ID",
"title": "名称",
"groupName": "分组名称",
"provider": "供应商",
@@ -60,7 +58,7 @@
"all": "全部",
"verifyLlmTitle": "请选择要检测的模型",
"testSuccess": "检测成功",
"searchTextPlaceholder": "搜索模型名称或名称",
"searchTextPlaceholder": "搜索模型 ID 或名称",
"modelAbility": {
"supportThinking": "推理",
"supportTool": "工具",
@@ -73,5 +71,4 @@
},
"requestPath": "请求路径",
"modelToBeTested": "待检测模型"
}

View File

@@ -2,7 +2,7 @@
"title": "大模型配置",
"modelOfChat": "对话模型供应商",
"dialogModel": "对话模型配置",
"modelName": "模型名称",
"modelName": "模型 ID",
"basic": "基本设置",
"updatePwd": "修改密码",
"systemAIFunctionSettings": "系统 AI 功能设置",

View File

@@ -1,11 +1,17 @@
<script setup lang="ts">
import type {llmType} from '#/api';
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
import {getDefaultModelAbility} from '#/views/ai/model/modelUtils/model-ability';
import type { llmType } from '#/api';
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
import {computed, onMounted, reactive, ref} from 'vue';
import { computed, onMounted, reactive, ref } from 'vue';
import {CircleCheck, CircleClose, Delete, Edit, Loading, Select,} from '@element-plus/icons-vue';
import {
CircleCheck,
CircleClose,
Delete,
Edit,
Loading,
Select,
} from '@element-plus/icons-vue';
import {
ElButton,
ElEmpty,
@@ -20,10 +26,11 @@ import {
ElTag,
} from 'element-plus';
import {deleteLlm, getModelList, verifyModelConfig,} from '#/api/ai/llm';
import {$t} from '#/locales';
import { deleteLlm, getModelList, verifyModelConfig } from '#/api/ai/llm';
import { $t } from '#/locales';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import {mapLlmToModelAbility} from '#/views/ai/model/modelUtils/model-ability-utils';
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
interface ProviderOption {
id: string;
@@ -480,7 +487,7 @@ defineExpose({
>
<ElTableColumn type="selection" width="48" />
<ElTableColumn label="模型名称" min-width="220">
<ElTableColumn label="模型 ID" min-width="220">
<template #default="{ row }">
<div class="active-workspace__name-with-logo">
<ModelProviderBadge

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
import { computed, reactive, ref, watch } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { IconifyIcon } from '@easyflow/icons';
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
import { api } from '#/api/request';
import { $t } from '#/locales';
import {
getDefaultModelAbility,
syncTagSelectedStatus as syncTagSelectedStatusUtil,
} from '#/views/ai/model/modelUtils/model-ability';
import {computed, reactive, ref, watch} from 'vue';
import {EasyFlowFormModal} from '@easyflow/common-ui';
import {ArrowDown, ArrowUp} from '@element-plus/icons-vue';
import {ElForm, ElFormItem, ElIcon, ElInput, ElMessage} from 'element-plus';
import {api} from '#/api/request';
import {$t} from '#/locales';
import {
generateFeaturesFromModelAbility,
resetModelAbility,
@@ -26,6 +26,8 @@ interface FormData {
modelName: string;
groupName: string;
providerId: string;
invokeCode: string;
publishEnabled: boolean;
apiKey: string;
endpoint: string;
requestPath: string;
@@ -63,7 +65,6 @@ const formDataRef = ref();
const isAdd = ref(true);
const dialogVisible = ref(false);
const btnLoading = ref(false);
const showAdvanced = ref(false);
const formData = reactive<FormData>({
modelType: '',
@@ -71,6 +72,8 @@ const formData = reactive<FormData>({
modelName: '',
groupName: '',
providerId: '',
invokeCode: '',
publishEnabled: false,
apiKey: '',
endpoint: '',
requestPath: '',
@@ -99,6 +102,17 @@ const modelTypeAbilityOptions = [
},
] as const;
const hasSpecialModelType = computed(() => Boolean(selectedModelType.value));
const abilityIconMap: Record<string, string> = {
embeddingModel: 'svg:knowledge',
rerankModel: 'svg:data-center',
thinking: 'svg:llm',
tool: 'svg:wrench',
video: 'mdi:video-outline',
image: 'mdi:image-outline',
audio: 'mdi:microphone-outline',
imageB64: 'mdi:file-image-outline',
toolMessage: 'mdi:hammer',
};
const syncTagSelectedStatus = () => {
syncTagSelectedStatusUtil(modelAbility.value, formData);
@@ -128,11 +142,20 @@ const handleModelTypeChipClick = (
};
const isAbilityChipDisabled = () => hasSpecialModelType.value;
const getAbilityIcon = (value: string) => abilityIconMap[value] || 'svg:llm';
const resolveModelType = (): FormData['modelType'] => {
return selectedModelType.value || 'chatModel';
};
const normalizeSelectableModelType = (
modelType?: string,
): SelectableModelType => {
return modelType === 'embeddingModel' || modelType === 'rerankModel'
? modelType
: '';
};
const resetFormData = () => {
Object.assign(formData, {
id: '',
@@ -141,6 +164,8 @@ const resetFormData = () => {
modelName: '',
groupName: '',
providerId: '',
invokeCode: '',
publishEnabled: false,
apiKey: '',
endpoint: '',
requestPath: '',
@@ -156,13 +181,16 @@ const resetFormData = () => {
};
defineExpose({
openAddDialog() {
openAddDialog(modelType?: string) {
isAdd.value = true;
formDataRef.value?.resetFields();
resetFormData();
showAdvanced.value = false;
selectedModelType.value = '';
resetAbilitySelection();
selectedModelType.value = normalizeSelectableModelType(modelType);
if (selectedModelType.value) {
resetAbilitySelection();
} else {
syncTagSelectedStatus();
}
dialogVisible.value = true;
},
@@ -176,6 +204,9 @@ defineExpose({
title: item.title || '',
modelName: item.modelName || '',
groupName: item.groupName || '',
providerId: item.providerId || '',
invokeCode: item.invokeCode || '',
publishEnabled: item.publishEnabled || false,
endpoint: item.endpoint || '',
requestPath: item.requestPath || '',
apiKey: item.apiKey || '',
@@ -189,13 +220,7 @@ defineExpose({
supportToolMessage:
item.supportToolMessage === undefined ? true : item.supportToolMessage,
});
selectedModelType.value =
item.modelType === 'embeddingModel' || item.modelType === 'rerankModel'
? item.modelType
: '';
showAdvanced.value = Boolean(
formData.apiKey || formData.endpoint || formData.requestPath,
);
selectedModelType.value = normalizeSelectableModelType(item.modelType);
if (selectedModelType.value) {
resetAbilitySelection();
} else {
@@ -281,11 +306,6 @@ const save = async () => {
class="model-modal__form"
>
<div class="model-modal__section">
<div class="model-modal__section-head">
<h4>基础信息</h4>
<p>这些字段决定模型在列表里的展示与组织方式</p>
</div>
<ElFormItem prop="title" :label="$t('llm.title')">
<ElInput
v-model.trim="formData.title"
@@ -304,81 +324,56 @@ const save = async () => {
placeholder="例如:默认组"
/>
</ElFormItem>
</div>
<div class="model-modal__section">
<div class="model-modal__section-head">
<h4>{{ $t('llm.ability') }}</h4>
<p>可选嵌入或重排模型类型选择后其余能力会自动锁定</p>
</div>
<div class="model-modal__ability">
<button
v-for="item in modelTypeAbilityOptions"
:key="item.value"
type="button"
class="model-modal__ability-chip"
:class="{ 'is-active': selectedModelType === item.value }"
@click="handleModelTypeChipClick(item.value)"
>
{{ item.label }}
</button>
</div>
<div class="model-modal__ability">
<button
v-for="item in modelAbility"
:key="item.value"
type="button"
class="model-modal__ability-chip"
:class="{
'is-active': item.selected,
'is-disabled': isAbilityChipDisabled(),
}"
:disabled="isAbilityChipDisabled()"
@click="handleTagClick(item)"
>
{{ item.label }}
</button>
</div>
</div>
<div class="model-modal__section">
<button
type="button"
class="model-modal__advanced-toggle"
@click="showAdvanced = !showAdvanced"
<ElFormItem
:label="$t('llm.ability')"
class="model-modal__ability-item"
>
<div>
<h4>高级设置</h4>
<p>仅在需要覆写服务商默认配置时填写</p>
<div class="model-modal__ability-panel">
<div class="model-modal__ability-toolbar">
<button
v-for="item in modelTypeAbilityOptions"
:key="item.value"
type="button"
class="model-modal__ability-chip"
:class="[
`is-tone-${item.value}`,
{ 'is-active': selectedModelType === item.value },
]"
@click="handleModelTypeChipClick(item.value)"
>
<IconifyIcon
:icon="getAbilityIcon(item.value)"
class="model-modal__ability-icon"
/>
{{ item.label }}
</button>
<span
class="model-modal__ability-separator"
aria-hidden="true"
></span>
<button
v-for="item in modelAbility"
:key="item.value"
type="button"
class="model-modal__ability-chip"
:class="{
'is-active': item.selected,
'is-disabled': isAbilityChipDisabled(),
[`is-tone-${item.value}`]: true,
}"
:disabled="isAbilityChipDisabled()"
@click="handleTagClick(item)"
>
<IconifyIcon
:icon="getAbilityIcon(item.value)"
class="model-modal__ability-icon"
/>
{{ item.label }}
</button>
</div>
</div>
<ElIcon>
<ArrowUp v-if="showAdvanced" />
<ArrowDown v-else />
</ElIcon>
</button>
<div v-if="showAdvanced" class="model-modal__advanced-grid">
<ElFormItem prop="apiKey" :label="$t('llmProvider.apiKey')">
<ElInput
v-model.trim="formData.apiKey"
type="password"
show-password
placeholder="可选,单独覆写模型密钥"
/>
</ElFormItem>
<ElFormItem prop="endpoint" :label="$t('llmProvider.endpoint')">
<ElInput
v-model.trim="formData.endpoint"
placeholder="可选,单独覆写 endpoint"
/>
</ElFormItem>
<ElFormItem prop="requestPath" :label="$t('llm.requestPath')">
<ElInput
v-model.trim="formData.requestPath"
placeholder="可选,单独覆写 requestPath"
/>
</ElFormItem>
</div>
</ElFormItem>
</div>
</ElForm>
</div>
@@ -389,114 +384,136 @@ const save = async () => {
.model-modal {
display: flex;
flex-direction: column;
gap: 20px;
gap: 16px;
}
.model-modal__section {
display: flex;
flex-direction: column;
gap: 14px;
padding: 20px;
background: linear-gradient(
180deg,
hsl(var(--surface-panel) / 98%) 0%,
hsl(var(--surface-contrast-soft) / 94%) 100%
);
border: 1px solid hsl(var(--divider-faint) / 50%);
border-radius: 24px;
}
.model-modal__section-head h4,
.model-modal__advanced-toggle h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: hsl(var(--text-strong));
}
.model-modal__section-head p,
.model-modal__advanced-toggle p {
margin: 6px 0 0;
font-size: 13px;
line-height: 1.6;
color: hsl(var(--text-muted));
gap: 12px;
padding: 16px 18px;
background: hsl(var(--surface-panel) / 96%);
border: 1px solid hsl(var(--divider-faint) / 42%);
border-radius: 20px;
}
.model-modal__form {
display: flex;
flex-direction: column;
gap: 18px;
gap: 12px;
}
.model-modal__ability {
.model-modal__ability-item {
margin-top: 4px;
}
.model-modal__ability-panel {
overflow: hidden;
padding: 2px;
border-radius: 18px;
}
.model-modal__ability-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.model-modal__ability-separator {
width: 1px;
align-self: stretch;
min-height: 28px;
background: hsl(var(--divider-faint) / 62%);
border-radius: 999px;
}
.model-modal__ability-chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 14px;
gap: 8px;
min-height: 40px;
padding: 9px 18px;
font-size: 13px;
font-weight: 600;
line-height: 1;
color: hsl(var(--text-muted));
cursor: pointer;
background: hsl(var(--surface-panel));
border: 1px solid hsl(var(--divider-faint) / 68%);
background: hsl(var(--surface-contrast-soft) / 86%);
border: 1px solid transparent;
border-radius: 999px;
transition:
transform 0.2s ease,
border-color 0.2s ease,
color 0.2s ease,
background 0.2s ease;
background 0.2s ease,
box-shadow 0.2s ease;
}
.model-modal__ability-chip:hover:not(:disabled),
.model-modal__ability-chip:focus-visible:not(:disabled) {
color: hsl(var(--text-strong));
border-color: hsl(var(--divider-faint));
transform: translateY(-1px);
box-shadow: 0 10px 18px -14px hsl(var(--foreground) / 28%);
}
.model-modal__ability-chip.is-active {
color: hsl(var(--text-strong));
background: hsl(var(--primary) / 8%);
border-color: hsl(var(--primary) / 38%);
border-color: hsl(var(--primary) / 18%);
background: hsl(var(--primary) / 10%);
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
}
.model-modal__ability-chip.is-disabled {
cursor: not-allowed;
opacity: 0.56;
box-shadow: none;
transform: none;
}
.model-modal__advanced-toggle {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0;
text-align: left;
background: transparent;
border: none;
.model-modal__ability-icon {
font-size: 15px;
opacity: 0.88;
}
.model-modal__advanced-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
.model-modal__ability-chip.is-active.is-tone-embeddingModel,
.model-modal__ability-chip.is-active.is-tone-thinking,
.model-modal__ability-chip.is-active.is-tone-toolMessage {
color: hsl(var(--primary));
background: hsl(var(--primary) / 10%);
border-color: hsl(var(--primary) / 18%);
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
}
.model-modal__advanced-grid :deep(.el-form-item:last-child) {
grid-column: 1 / -1;
.model-modal__ability-chip.is-active.is-tone-rerankModel,
.model-modal__ability-chip.is-active.is-tone-tool {
color: hsl(var(--warning));
background: hsl(var(--warning) / 12%);
border-color: hsl(var(--warning) / 18%);
box-shadow: inset 0 0 0 1px hsl(var(--warning) / 14%);
}
.model-modal__ability-chip.is-active.is-tone-image,
.model-modal__ability-chip.is-active.is-tone-imageB64 {
color: hsl(var(--success));
background: hsl(var(--success) / 12%);
border-color: hsl(var(--success) / 18%);
box-shadow: inset 0 0 0 1px hsl(var(--success) / 14%);
}
.model-modal__ability-chip.is-active.is-tone-audio,
.model-modal__ability-chip.is-active.is-tone-video,
.model-modal__ability-chip.is-active.is-tone-free {
color: hsl(var(--danger));
background: hsl(var(--danger) / 10%);
border-color: hsl(var(--danger) / 16%);
box-shadow: inset 0 0 0 1px hsl(var(--danger) / 12%);
}
@media (max-width: 640px) {
.model-modal__advanced-grid {
grid-template-columns: minmax(0, 1fr);
}
.model-modal__advanced-grid :deep(.el-form-item:last-child) {
grid-column: auto;
.model-modal__ability-separator {
display: none;
}
}
</style>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import {computed, onMounted, ref} from 'vue';
import { computed, onMounted, ref } from 'vue';
import {$t} from '@easyflow/locales';
import { $t } from '@easyflow/locales';
import {Delete, Edit, Plus, Select, Setting} from '@element-plus/icons-vue';
import { Delete, Edit, Plus, Select, Setting } from '@element-plus/icons-vue';
import {
ElButton,
ElEmpty,
@@ -15,15 +15,15 @@ import {
ElTag,
} from 'element-plus';
import {getLlmProviderList} from '#/api/ai/llm.js';
import {api} from '#/api/request.js';
import { getLlmProviderList } from '#/api/ai/llm.js';
import { api } from '#/api/request.js';
import ListPageShell from '#/components/page/ListPageShell.vue';
import ActiveModelWorkspace from '#/views/ai/model/ActiveModelWorkspace.vue';
import AddModelModal from '#/views/ai/model/AddModelModal.vue';
import AddModelProviderModal from '#/views/ai/model/AddModelProviderModal.vue';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import {getProviderPresetByValue} from '#/views/ai/model/modelUtils/defaultIcon';
import {modelTypes} from '#/views/ai/model/modelUtils/modelTypes';
import { getProviderPresetByValue } from '#/views/ai/model/modelUtils/defaultIcon';
import { modelTypes } from '#/views/ai/model/modelUtils/modelTypes';
import {
createProviderDraft,
getProviderConfigMetrics,
@@ -31,8 +31,9 @@ import {
} from '#/views/ai/model/modelUtils/providerDraft';
import ModelVerifyConfig from '#/views/ai/model/ModelVerifyConfig.vue';
import ModelViewItemOperation from '#/views/ai/model/ModelViewItemOperation.vue';
import UnifiedGatewayWorkspace from '#/views/ai/model/UnifiedGatewayWorkspace.vue';
type ModelWorkspaceView = 'active' | 'provider';
type ModelWorkspaceView = 'active' | 'gateway' | 'provider';
interface ModelGroup {
groupName: string;
@@ -59,6 +60,7 @@ const addLlmProviderRef = ref();
const llmVerifyConfigRef = ref();
const addLlmRef = ref();
const activeWorkspaceRef = ref();
const unifiedGatewayWorkspaceRef = ref();
const selectedProvider = computed(() =>
providers.value.find((item) => item.id === selectedProviderId.value),
@@ -194,7 +196,10 @@ const loadProviderDetail = async (
isDetailLoading.value = true;
try {
const res = await api.get(`/api/v1/model/getList?providerId=${providerId}`, {});
const res = await api.get(
`/api/v1/model/getList?providerId=${providerId}`,
{},
);
if (res.errorCode === 0) {
syncGroupedModels(res.data);
@@ -282,15 +287,30 @@ const switchWorkspaceView = async (target: ModelWorkspaceView) => {
return;
}
if (!(await confirmDiscardProviderDraft())) {
if (
workspaceView.value === 'provider' &&
!(await confirmDiscardProviderDraft())
) {
return;
}
if (workspaceView.value === 'gateway') {
const canLeaveGateway =
(await unifiedGatewayWorkspaceRef.value?.confirmBeforeLeave?.()) ?? true;
if (!canLeaveGateway) {
return;
}
}
workspaceView.value = target;
if (target === 'active') {
await activeWorkspaceRef.value?.reloadData?.();
}
if (target === 'gateway') {
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
}
};
const selectProvider = async (provider: any) => {
@@ -357,6 +377,7 @@ const handleDeleteProvider = async (provider: any) => {
const remaining = providers.value.find((item) => item.id !== provider.id);
await loadProviders(remaining?.id, true);
await activeWorkspaceRef.value?.reloadData?.();
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
}
};
@@ -375,6 +396,7 @@ const openEditProviderDialog = () => {
const handleProviderModalReload = async () => {
await loadProviders(selectedProviderId.value, true);
await activeWorkspaceRef.value?.reloadData?.();
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
};
const handleAddLlm = (modelType = activeModelType.value) => {
@@ -444,6 +466,7 @@ const handleModelDataReload = async () => {
keepDraft: isProviderDirty.value,
});
await activeWorkspaceRef.value?.reloadData?.();
await unifiedGatewayWorkspaceRef.value?.reloadData?.();
await refreshProviderCounts(providers.value);
};
@@ -481,6 +504,14 @@ onMounted(() => {
>
已配置模型
</button>
<button
type="button"
class="workspace-switch__item"
:class="{ 'is-active': workspaceView === 'gateway' }"
@click="switchWorkspaceView('gateway')"
>
统一网关
</button>
</div>
</template>
@@ -770,7 +801,7 @@ onMounted(() => {
</section>
<ActiveModelWorkspace
v-else
v-else-if="workspaceView === 'active'"
ref="activeWorkspaceRef"
:providers="providers"
@create-model="handleAddLlm"
@@ -778,6 +809,12 @@ onMounted(() => {
@refresh-provider-stats="handleActiveProviderStatsRefresh"
/>
<UnifiedGatewayWorkspace
v-else
ref="unifiedGatewayWorkspaceRef"
@updated="handleModelDataReload"
/>
<AddModelProviderModal
ref="addLlmProviderRef"
@reload="handleProviderModalReload"

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import {computed} from 'vue';
import { computed } from 'vue';
import {ElImage} from 'element-plus';
import { ElImage } from 'element-plus';
import {
getIconByValue,

View File

@@ -1,17 +1,25 @@
<script setup lang="ts">
import type {PropType} from 'vue';
import {ref} from 'vue';
import type { PropType } from 'vue';
import type {llmType} from '#/api';
import type {ModelAbilityItem} from '#/views/ai/model/modelUtils/model-ability';
import {getDefaultModelAbility} from '#/views/ai/model/modelUtils/model-ability';
import type { llmType } from '#/api';
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
import {CircleCheck, CircleClose, Delete, Edit, Loading, Select,} from '@element-plus/icons-vue';
import {ElButton, ElIcon, ElMessage, ElTag} from 'element-plus';
import { ref } from 'vue';
import {verifyModelConfig} from '#/api/ai/llm';
import {
CircleCheck,
CircleClose,
Delete,
Edit,
Loading,
Select,
} from '@element-plus/icons-vue';
import { ElButton, ElIcon, ElMessage, ElTag } from 'element-plus';
import { verifyModelConfig } from '#/api/ai/llm';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import {mapLlmToModelAbility} from '#/views/ai/model/modelUtils/model-ability-utils';
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
const props = defineProps({
llmList: {
@@ -115,14 +123,6 @@ const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
const allTags = mapLlmToModelAbility(llm, defaultAbility);
return allTags.filter((tag) => tag.selected);
};
const getModelMeta = (llm: llmType) => {
const providerName =
llm?.modelProvider?.providerName || llm?.aiLlmProvider?.providerName || '';
const modelName = llm.llmModel || llm.title;
return `${providerName} · ${modelName}`;
};
</script>
<template>
@@ -153,7 +153,6 @@ const getModelMeta = (llm: llmType) => {
</ElTag>
</div>
</div>
<p class="llm-item__meta">{{ getModelMeta(llm) }}</p>
<p v-if="llm.description" class="llm-item__description">
{{ llm.description }}
</p>
@@ -263,7 +262,6 @@ const getModelMeta = (llm: llmType) => {
color: hsl(var(--text-strong));
}
.llm-item__meta,
.llm-item__description {
margin: 0;
font-size: 13px;

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,20 @@
import {describe, expect, it} from 'vitest';
import { describe, expect, it } from 'vitest';
import {getProviderBadgeText, getProviderPresetByValue} from '../defaultIcon';
import { getProviderBadgeText, getProviderPresetByValue } from '../defaultIcon';
describe('defaultIcon helpers', () => {
it('使用 lobehub 官方静态 svg 作为默认服务商 logo', () => {
expect(getProviderPresetByValue('openai')?.icon).toBe(
'/model-providers/lobehub/openai.svg',
);
expect(getProviderPresetByValue('deepseek')?.icon).toBe(
'/model-providers/lobehub/deepseek.svg',
);
expect(getProviderPresetByValue('self-hosted')?.icon).toBe(
'/model-providers/lobehub/vllm.svg',
);
});
it('可以读取新的服务商预设', () => {
const preset = getProviderPresetByValue('self-hosted');

View File

@@ -2,7 +2,6 @@ import { $t } from '#/locales';
export type BooleanField =
| 'supportAudio'
| 'supportFree'
| 'supportImage'
| 'supportImageB64Only'
| 'supportThinking'
@@ -56,14 +55,6 @@ export const getDefaultModelAbility = (): ModelAbilityItem[] => [
selected: false,
field: 'supportImage',
},
{
label: $t('llm.modelAbility.supportFree'),
value: 'free',
defaultType: 'info',
activeType: 'success',
selected: false,
field: 'supportFree',
},
{
label: $t('llm.modelAbility.supportAudio'),
value: 'audio',
@@ -126,7 +117,6 @@ export const syncTagSelectedStatus = (
/**
* 处理标签点击事件
* @param modelAbility 模型能力数组
* @param item 被点击的标签项
* @param formData 表单数据对象
*/
@@ -165,5 +155,4 @@ export const getAllBooleanFields = (): BooleanField[] => [
'supportImageB64Only',
'supportVideo',
'supportAudio',
'supportFree',
];

View File

@@ -25,7 +25,7 @@
}
]
},
"icon": "/model-providers/deepseek.png"
"icon": "/model-providers/lobehub/deepseek.svg"
},
{
"label": "OpenAI",
@@ -62,7 +62,7 @@
}
]
},
"icon": "/model-providers/openai.svg"
"icon": "/model-providers/lobehub/openai.svg"
},
{
"label": "阿里百炼",
@@ -98,7 +98,7 @@
}
]
},
"icon": "/model-providers/aliyun.png"
"icon": "/model-providers/lobehub/bailian.svg"
},
{
"label": "智谱",
@@ -133,7 +133,7 @@
}
]
},
"icon": "/model-providers/zhipu.png"
"icon": "/model-providers/lobehub/zhipu.svg"
},
{
"label": "MiniMax",
@@ -161,7 +161,7 @@
}
]
},
"icon": "/model-providers/minimax.png"
"icon": "/model-providers/lobehub/minimax.svg"
},
{
"label": "Kimi",
@@ -189,7 +189,7 @@
}
]
},
"icon": "/model-providers/kimi.png"
"icon": "/model-providers/lobehub/kimi.svg"
},
{
"label": "硅基流动",
@@ -225,7 +225,7 @@
}
]
},
"icon": "/model-providers/siliconflow.png"
"icon": "/model-providers/lobehub/siliconcloud.svg"
},
{
"label": "Ollama",
@@ -253,7 +253,7 @@
}
]
},
"icon": "/model-providers/ollama.png"
"icon": "/model-providers/lobehub/ollama.svg"
},
{
"label": "自部署",
@@ -288,6 +288,6 @@
}
]
},
"icon": "/model-providers/self-hosted.svg"
"icon": "/model-providers/lobehub/vllm.svg"
}
]

View File

@@ -1,45 +1,43 @@
{
"filed":
{
"id": "Id",
"deptId": "DeptId",
"tenantId": "TenantId",
"title": "Title",
"brand": "Brand",
"icon": "Icon",
"description": "Description",
"supportChat": "SupportChat",
"supportFunctionCalling": "SupportFunctionCalling",
"supportEmbed": "SupportEmbed",
"supportReranker": "SupportReranker",
"supportTextToImage": "SupportTextToImage",
"supportImageToImage": "SupportImageToImage",
"supportTextToAudio": "SupportTextToAudio",
"supportAudioToAudio": "SupportAudioToAudio",
"supportTextToVideo": "SupportTextToVideo",
"supportImageToVideo": "SupportImageToVideo",
"multimodal": "multimodal",
"llmEndpoint": "LlmEndpoint",
"chatPath": "ChatPath",
"embedPath": "embedPath",
"llmModel": "LlmModel",
"llmApiKey": "apiKey",
"llmExtraConfig": "LlmExtraConfig",
"options": "Options",
"ability": "Ability"
}
,
"filed": {
"id": "Id",
"deptId": "DeptId",
"tenantId": "TenantId",
"title": "Title",
"brand": "Brand",
"icon": "Icon",
"description": "Description",
"supportChat": "SupportChat",
"supportFunctionCalling": "SupportFunctionCalling",
"supportEmbed": "SupportEmbed",
"supportReranker": "SupportReranker",
"supportTextToImage": "SupportTextToImage",
"supportImageToImage": "SupportImageToImage",
"supportTextToAudio": "SupportTextToAudio",
"supportAudioToAudio": "SupportAudioToAudio",
"supportTextToVideo": "SupportTextToVideo",
"supportImageToVideo": "SupportImageToVideo",
"multimodal": "multimodal",
"llmEndpoint": "LlmEndpoint",
"chatPath": "ChatPath",
"embedPath": "embedPath",
"llmModel": "Model ID",
"llmApiKey": "apiKey",
"llmExtraConfig": "LlmExtraConfig",
"options": "Options",
"ability": "Ability"
},
"llmModal": {
"TitleRequired": "Please enter the name",
"BrandRequired": "Please enter the brand",
"ModelRequired": "Please enter the model",
"ModelRequired": "Please enter the model ID",
"ApiKeyRequired": "Please enter the apiKey",
"QuickAddLlm": "One-click addition of large models"
},
"placeholder": {
"title": "Please enter the title",
"brand": "Please enter the brand",
"llmModel": "Please enter the llmModel",
"llmModel": "Please enter the model ID",
"description": "Please enter the description"
},
"actions": {
@@ -50,7 +48,7 @@
},
"addProvider": "Add Provider",
"modelType": "ModelType",
"llmModel": "ModelName",
"llmModel": "Model ID",
"title": "name",
"groupName": "GroupName",
"provider": "供应商",

View File

@@ -2,7 +2,7 @@
"title": "Large Model Configuration",
"modelOfChat": "Chat Model Provider",
"dialogModel": "Chat Model Settings",
"modelName": "Model Name",
"modelName": "Model ID",
"basic": "BasicInformation",
"updatePwd": "UpdatePassword"
}

View File

@@ -1,9 +1,8 @@
{
"filed":
{
"filed": {
"title": "标题",
"brand": "供应商",
"llmModel": "模型名称",
"llmModel": "模型 ID",
"icon": "ICON",
"description": "描述",
"supportChat": "对话模型",
@@ -24,19 +23,18 @@
"llmExtraConfig": "大模型其他属性配置",
"options": "其他配置内容",
"ability": "能力"
}
,
},
"llmModal": {
"TitleRequired": "请输入名称",
"BrandRequired": "请选择供应商",
"ModelRequired": "请输入模型",
"ModelRequired": "请输入模型 ID",
"ApiKeyRequired": "请输入apiKey",
"QuickAddLlm": "一键添加大模型"
},
"placeholder": {
"title": "请输入名称",
"brand": "请选择品牌",
"llmModel": "请输入模型名称",
"llmModel": "请输入模型 ID",
"description": "请输入描述"
},
"actions": {
@@ -47,7 +45,7 @@
},
"addProvider": "添加供应商",
"modelType": "模型类型",
"llmModel": "模型名称",
"llmModel": "模型 ID",
"title": "名称",
"groupName": "分组名称",
"provider": "供应商",

View File

@@ -1,8 +1,8 @@
{
"title": "大模型配置",
"modelOfChat": "对话模型供应商",
"dialogModel" : "对话模型配置",
"modelName": "模型名称",
"dialogModel": "对话模型配置",
"modelName": "模型 ID",
"basic": "基本设置",
"updatePwd": "修改密码"
}

View File

@@ -173,6 +173,11 @@
<artifactId>easy-agents-support</artifactId>
<version>${easy-agents.version}</version>
</dependency>
<dependency>
<groupId>com.easyagents</groupId>
<artifactId>easy-agents-spring-boot-starter</artifactId>
<version>${easy-agents.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>