Browse Source

适配qmth-boot:core-ai,增加相关配置功能及ai接口实现

luoshi 1 year ago
parent
commit
20f7377db2
32 changed files with 1260 additions and 5 deletions
  1. 9 0
      pom.xml
  2. 59 0
      src/main/java/com/qmth/ops/api/controller/ai/LlmController.java
  3. 32 0
      src/main/java/com/qmth/ops/api/security/AccessOrg.java
  4. 28 0
      src/main/java/com/qmth/ops/api/security/AiAuthorizationService.java
  5. 72 0
      src/main/java/com/qmth/ops/biz/ai/client/ChatApiClient.java
  6. 69 0
      src/main/java/com/qmth/ops/biz/ai/client/ChatApiConfig.java
  7. 32 0
      src/main/java/com/qmth/ops/biz/ai/client/Credentials.java
  8. 73 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatClient.java
  9. 18 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatInput.java
  10. 20 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatOutput.java
  11. 47 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatRequest.java
  12. 34 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatResult.java
  13. 39 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunError.java
  14. 30 0
      src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunUsage.java
  15. 69 0
      src/main/java/com/qmth/ops/biz/ai/client/azure/AzureChatClient.java
  16. 47 0
      src/main/java/com/qmth/ops/biz/ai/client/azure/AzureChatResult.java
  17. 41 0
      src/main/java/com/qmth/ops/biz/ai/client/azure/AzureUsage.java
  18. 12 0
      src/main/java/com/qmth/ops/biz/ai/exception/ChatClientNotFound.java
  19. 12 0
      src/main/java/com/qmth/ops/biz/ai/exception/ChatRateLimitExceeded.java
  20. 12 0
      src/main/java/com/qmth/ops/biz/ai/exception/ChatRequestError.java
  21. 8 0
      src/main/java/com/qmth/ops/biz/dao/LlmModelDao.java
  22. 17 0
      src/main/java/com/qmth/ops/biz/dao/LlmOrgConfigDao.java
  23. 8 0
      src/main/java/com/qmth/ops/biz/dao/LlmSupplierDao.java
  24. 84 0
      src/main/java/com/qmth/ops/biz/domain/LlmModel.java
  25. 62 0
      src/main/java/com/qmth/ops/biz/domain/LlmOrgConfig.java
  26. 84 0
      src/main/java/com/qmth/ops/biz/domain/LlmSupplier.java
  27. 64 0
      src/main/java/com/qmth/ops/biz/service/LlmClientService.java
  28. 41 0
      src/main/java/com/qmth/ops/biz/service/LlmModelService.java
  29. 46 0
      src/main/java/com/qmth/ops/biz/service/LlmOrgConfigService.java
  30. 41 0
      src/main/java/com/qmth/ops/biz/service/LlmSupplierService.java
  31. 5 0
      src/main/java/com/qmth/ops/biz/service/OrgService.java
  32. 45 5
      src/main/resources/script/init.sql

+ 9 - 0
pom.xml

@@ -46,6 +46,10 @@
             <groupId>com.qmth.boot</groupId>
             <artifactId>core-sms</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.qmth.boot</groupId>
+            <artifactId>core-ai</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.qmth.boot</groupId>
             <artifactId>core-solar</artifactId>
@@ -89,6 +93,11 @@
                 <artifactId>core-fss</artifactId>
                 <version>${qmth.boot.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.qmth.boot</groupId>
+                <artifactId>core-ai</artifactId>
+                <version>${qmth.boot.version}</version>
+            </dependency>
             <dependency>
                 <groupId>com.qmth.boot</groupId>
                 <artifactId>core-solar</artifactId>

+ 59 - 0
src/main/java/com/qmth/ops/api/controller/ai/LlmController.java

@@ -0,0 +1,59 @@
+package com.qmth.ops.api.controller.ai;
+
+import com.qmth.boot.api.annotation.Aac;
+import com.qmth.boot.core.ai.model.AiConstants;
+import com.qmth.boot.core.ai.model.llm.*;
+import com.qmth.boot.core.exception.ForbiddenException;
+import com.qmth.boot.tools.signature.SignatureType;
+import com.qmth.ops.api.security.AccessOrg;
+import com.qmth.ops.biz.domain.LlmOrgConfig;
+import com.qmth.ops.biz.service.LlmClientService;
+import com.qmth.ops.biz.service.LlmOrgConfigService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+
+@RestController
+@Aac(auth = true, signType = SignatureType.SECRET)
+public class LlmController {
+
+    @Resource
+    private LlmOrgConfigService llmOrgConfigService;
+
+    @Resource
+    private LlmClientService llmClientService;
+
+    @PostMapping(AiConstants.LLM_CHAT_PATH)
+    public ChatResult chat(@RequestAttribute AccessOrg accessOrg,
+            @RequestHeader(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type,
+            @RequestBody @Validated ChatRequest request) throws Exception {
+        LlmOrgConfig config = llmOrgConfigService.findByOrgAndAppType(accessOrg.getOrg(), type);
+        if (config != null && config.getLeftCount() > 0) {
+            ChatResult result = llmClientService.chat(request, config.getModelId());
+            llmOrgConfigService.consume(config);
+            return result;
+        } else {
+            throw new ForbiddenException(
+                    "Chat api is disabled or exhausted for org=" + accessOrg.getOrg().getCode() + ", app type=" + type);
+        }
+    }
+
+    @PostMapping(AiConstants.LLM_BALANCE_PATH)
+    public LlmAppBalance balance(@RequestAttribute AccessOrg accessOrg,
+            @RequestHeader(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type) {
+        LlmOrgConfig config = llmOrgConfigService.findByOrgAndAppType(accessOrg.getOrg(), type);
+        LlmAppBalance balance = new LlmAppBalance();
+        if (config != null) {
+            balance.setPermitCount(config.getPermitCount());
+            balance.setLeftCount(config.getLeftCount());
+        }
+        return balance;
+    }
+
+    @PostMapping(AiConstants.LLM_PROMPT_TEMPLATE_PATH)
+    public PromptTemplate getPromptTemplate(@RequestAttribute AccessOrg accessOrg,
+            @RequestHeader(AiConstants.LLM_APP_TYPE_HEADER) LlmAppType type) {
+        return null;
+    }
+}

+ 32 - 0
src/main/java/com/qmth/ops/api/security/AccessOrg.java

@@ -0,0 +1,32 @@
+package com.qmth.ops.api.security;
+
+import com.qmth.boot.core.security.model.AccessEntity;
+import com.qmth.ops.biz.domain.Org;
+
+public class AccessOrg implements AccessEntity {
+
+    private Org org;
+
+    public AccessOrg(Org org) {
+        this.org = org;
+    }
+
+    @Override
+    public String getIdentity() {
+        return org.getAccessKey();
+    }
+
+    @Override
+    public String getSecret() {
+        return org.getAccessSecret();
+    }
+
+    public void setOrg(Org org) {
+        this.org = org;
+    }
+
+    public Org getOrg() {
+        return org;
+    }
+
+}

+ 28 - 0
src/main/java/com/qmth/ops/api/security/AiAuthorizationService.java

@@ -0,0 +1,28 @@
+package com.qmth.ops.api.security;
+
+import com.qmth.boot.core.ai.model.AiConstants;
+import com.qmth.boot.core.security.annotation.AuthorizationComponent;
+import com.qmth.boot.core.security.service.AuthorizationService;
+import com.qmth.boot.tools.signature.SignatureType;
+import com.qmth.ops.api.constants.OpsApiConstants;
+import com.qmth.ops.biz.domain.Org;
+import com.qmth.ops.biz.service.OrgService;
+
+import javax.annotation.Resource;
+
+@AuthorizationComponent(prefix = { AiConstants.API_PREFIX }, type = SignatureType.SECRET)
+public class AiAuthorizationService implements AuthorizationService<AccessOrg>, OpsApiConstants {
+
+    @Resource
+    private OrgService orgService;
+
+    @Override
+    public AccessOrg findByIdentity(String identity, SignatureType signatureType, String path) {
+        Org org = orgService.findByAccessKey(identity);
+        if (org != null && org.isEnable()) {
+            return new AccessOrg(org);
+        }
+        return null;
+    }
+
+}

+ 72 - 0
src/main/java/com/qmth/ops/biz/ai/client/ChatApiClient.java

@@ -0,0 +1,72 @@
+package com.qmth.ops.biz.ai.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmth.boot.core.ai.model.llm.ChatRequest;
+import com.qmth.boot.core.ai.model.llm.ChatResult;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+import com.qmth.boot.core.rateLimit.service.impl.MemoryRateLimiter;
+import com.qmth.ops.biz.ai.exception.ChatRateLimitExceeded;
+import okhttp3.*;
+
+import java.io.IOException;
+
+/**
+ * 大模型chat类接口基础实现
+ */
+public abstract class ChatApiClient {
+
+    private OkHttpClient client;
+
+    private ObjectMapper mapper;
+
+    private ChatApiConfig config;
+
+    private RateLimiter rateLimiter;
+
+    public ChatApiClient(ChatApiConfig config) {
+        this.client = new OkHttpClient.Builder().connectionPool(new ConnectionPool()).build();
+        this.mapper = new ObjectMapper();
+        this.config = config;
+        if (config.getQpm() > 0) {
+            this.rateLimiter = new MemoryRateLimiter(config.getQpm(), 60 * 1000);
+        }
+    }
+
+    protected ChatApiConfig getConfig() {
+        return config;
+    }
+
+    protected abstract Credentials gerCredentials();
+
+    protected abstract Object buildRequest(ChatRequest request);
+
+    protected abstract ChatResult buildResult(byte[] data, ObjectMapper mapper) throws IOException;
+
+    protected abstract ChatResult handleError(byte[] data, int statusCode, ObjectMapper mapper);
+
+    public ChatResult call(ChatRequest request) throws Exception {
+        if (rateLimiter != null && !rateLimiter.acquire()) {
+            throw new ChatRateLimitExceeded(config.getSupplier(), config.getModel(), config.getQpm());
+        }
+        try {
+            RequestBody body = RequestBody
+                    .create(MediaType.parse("application/json"), mapper.writeValueAsBytes(buildRequest(request)));
+            Credentials credentials = gerCredentials();
+            Request httpRequest = new Request.Builder().url(config.getUrl())
+                    .addHeader(credentials.getName(), credentials.getValue()).post(body).build();
+
+            Response response = client.newCall(httpRequest).execute();
+            byte[] data = response.body() != null ? response.body().bytes() : null;
+            if (response.isSuccessful()) {
+                return buildResult(data, mapper);
+            } else {
+                return handleError(data, response.code(), mapper);
+            }
+        } finally {
+            if (rateLimiter != null) {
+                rateLimiter.release();
+            }
+        }
+    }
+
+}

+ 69 - 0
src/main/java/com/qmth/ops/biz/ai/client/ChatApiConfig.java

@@ -0,0 +1,69 @@
+package com.qmth.ops.biz.ai.client;
+
+import com.qmth.ops.biz.domain.LlmModel;
+import com.qmth.ops.biz.domain.LlmSupplier;
+
+public class ChatApiConfig {
+
+    private String supplier;
+
+    private String url;
+
+    private String secret;
+
+    private String model;
+
+    private int qpm;
+
+    public ChatApiConfig() {
+
+    }
+
+    public ChatApiConfig(LlmSupplier supplier, LlmModel model) {
+        this.supplier = supplier.getName();
+        this.url = supplier.getUrl();
+        this.secret = supplier.getSecret();
+        this.model = model.getName();
+        this.qpm = model.getQpm();
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public String getSupplier() {
+        return supplier;
+    }
+
+    public void setSupplier(String supplier) {
+        this.supplier = supplier;
+    }
+
+    public int getQpm() {
+        return qpm;
+    }
+
+    public void setQpm(int qpm) {
+        this.qpm = qpm;
+    }
+}

+ 32 - 0
src/main/java/com/qmth/ops/biz/ai/client/Credentials.java

@@ -0,0 +1,32 @@
+package com.qmth.ops.biz.ai.client;
+
+/**
+ * API访问凭证信息,用于设置header
+ */
+public class Credentials {
+
+    private String name;
+
+    private String value;
+
+    public Credentials(String name, String value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+}

+ 73 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatClient.java

@@ -0,0 +1,73 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmth.boot.core.ai.model.llm.ChatRequest;
+import com.qmth.boot.core.ai.model.llm.ChatResult;
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+import com.qmth.boot.core.exception.ReentrantException;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.ops.biz.ai.client.ChatApiClient;
+import com.qmth.ops.biz.ai.client.ChatApiConfig;
+import com.qmth.ops.biz.ai.client.Credentials;
+import com.qmth.ops.biz.ai.exception.ChatRequestError;
+
+import java.io.IOException;
+
+public class AliyunChatClient extends ChatApiClient {
+
+    private static final String AUTH_HEADER_NAME = "Authorization";
+
+    private static final String AUTH_HEADER_VALUE = "Bearer ";
+
+    public AliyunChatClient(ChatApiConfig config) {
+        super(config);
+    }
+
+    @Override
+    protected Credentials gerCredentials() {
+        return new Credentials(AUTH_HEADER_NAME, AUTH_HEADER_VALUE + getConfig().getSecret());
+    }
+
+    @Override
+    protected Object buildRequest(ChatRequest request) {
+        return new AliyunChatRequest(request, getConfig().getModel());
+    }
+
+    @Override
+    protected ChatResult buildResult(byte[] data, ObjectMapper mapper) throws IOException {
+        return mapper.readValue(data, AliyunChatResult.class).buildResult();
+    }
+
+    @Override
+    protected ChatResult handleError(byte[] data, int statusCode, ObjectMapper mapper) {
+        AliyunError error = null;
+        if (data != null) {
+            try {
+                error = mapper.readValue(data, AliyunError.class);
+            } catch (Exception e) {
+            }
+        }
+        switch (statusCode) {
+        case 400:
+            throw new ChatRequestError(error != null ? error.getMessage() : "chat request error");
+        case 429:
+            throw new ReentrantException(error != null ? error.getMessage() : "chat api rate limit exceeded");
+        default:
+            throw new StatusException(error != null ? error.getMessage() : "chat api error");
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        ChatApiConfig config = new ChatApiConfig();
+        config.setSupplier("aliyun");
+        config.setUrl("https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation");
+        config.setSecret("sk-94599ccd34e240de8818310387386d91");
+        config.setModel("qwen1.5-7b-chat");
+        config.setQpm(0);
+        AliyunChatClient client = new AliyunChatClient(config);
+        ChatRequest request = new ChatRequest();
+        request.addMessage(ChatRole.user, "who are you");
+        System.out.println(new ObjectMapper().writeValueAsString(client.call(request)));
+    }
+
+}

+ 18 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatInput.java

@@ -0,0 +1,18 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.qmth.boot.core.ai.model.llm.ChatMessage;
+
+import java.util.List;
+
+public class AliyunChatInput {
+
+    private List<ChatMessage> messages;
+
+    public List<ChatMessage> getMessages() {
+        return messages;
+    }
+
+    public void setMessages(List<ChatMessage> messages) {
+        this.messages = messages;
+    }
+}

+ 20 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatOutput.java

@@ -0,0 +1,20 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.qmth.boot.core.ai.model.llm.ChatChoice;
+
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AliyunChatOutput {
+
+    private List<ChatChoice> choices;
+
+    public List<ChatChoice> getChoices() {
+        return choices;
+    }
+
+    public void setChoices(List<ChatChoice> choices) {
+        this.choices = choices;
+    }
+}

+ 47 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatRequest.java

@@ -0,0 +1,47 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.qmth.boot.core.ai.model.llm.ChatRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AliyunChatRequest {
+
+    private String model;
+
+    private AliyunChatInput input;
+
+    private Map<String, Object> parameters;
+
+    public AliyunChatRequest(ChatRequest request, String model) {
+        this.model = model;
+        this.input = new AliyunChatInput();
+        this.input.setMessages(request.getMessages());
+        this.parameters = new HashMap<>();
+        this.parameters.put("result_format", "message");
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public AliyunChatInput getInput() {
+        return input;
+    }
+
+    public void setInput(AliyunChatInput input) {
+        this.input = input;
+    }
+
+    public Map<String, Object> getParameters() {
+        return parameters;
+    }
+
+    public void setParameters(Map<String, Object> parameters) {
+        this.parameters = parameters;
+    }
+}

+ 34 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunChatResult.java

@@ -0,0 +1,34 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.qmth.boot.core.ai.model.llm.ChatResult;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AliyunChatResult {
+
+    private AliyunChatOutput output;
+
+    private AliyunUsage usage;
+
+    public ChatResult buildResult() {
+        ChatResult result = new ChatResult();
+        result.setChoices(output.getChoices());
+        return result;
+    }
+
+    public AliyunChatOutput getOutput() {
+        return output;
+    }
+
+    public void setOutput(AliyunChatOutput output) {
+        this.output = output;
+    }
+
+    public AliyunUsage getUsage() {
+        return usage;
+    }
+
+    public void setUsage(AliyunUsage usage) {
+        this.usage = usage;
+    }
+}

+ 39 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunError.java

@@ -0,0 +1,39 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AliyunError {
+
+    @JsonProperty("request_id")
+    private String requestId;
+
+    private String code;
+
+    private String message;
+
+    public String getRequestId() {
+        return requestId;
+    }
+
+    public void setRequestId(String requestId) {
+        this.requestId = requestId;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}

+ 30 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunUsage.java

@@ -0,0 +1,30 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AliyunUsage {
+
+    @JsonProperty("input_tokens")
+    private int inputTokens;
+
+    @JsonProperty("output_tokens")
+    private int outputTokens;
+
+    public int getInputTokens() {
+        return inputTokens;
+    }
+
+    public void setInputTokens(int inputTokens) {
+        this.inputTokens = inputTokens;
+    }
+
+    public int getOutputTokens() {
+        return outputTokens;
+    }
+
+    public void setOutputTokens(int outputTokens) {
+        this.outputTokens = outputTokens;
+    }
+}

+ 69 - 0
src/main/java/com/qmth/ops/biz/ai/client/azure/AzureChatClient.java

@@ -0,0 +1,69 @@
+package com.qmth.ops.biz.ai.client.azure;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmth.boot.core.ai.model.llm.ChatRequest;
+import com.qmth.boot.core.ai.model.llm.ChatResult;
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+import com.qmth.boot.core.exception.NotFoundException;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.ops.biz.ai.client.ChatApiClient;
+import com.qmth.ops.biz.ai.client.ChatApiConfig;
+import com.qmth.ops.biz.ai.client.Credentials;
+import com.qmth.ops.biz.ai.exception.ChatRequestError;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class AzureChatClient extends ChatApiClient {
+
+    private static final String AUTH_HEADER_NAME = "api-key";
+
+    private static final String MODEL_PLACEHOLDER = "{model}";
+
+    public AzureChatClient(ChatApiConfig config) {
+        super(config);
+        config.setUrl(config.getUrl().replace(MODEL_PLACEHOLDER, config.getModel()));
+    }
+
+    @Override
+    protected Credentials gerCredentials() {
+        return new Credentials(AUTH_HEADER_NAME, getConfig().getSecret());
+    }
+
+    @Override
+    protected Object buildRequest(ChatRequest request) {
+        return request;
+    }
+
+    @Override
+    protected ChatResult buildResult(byte[] data, ObjectMapper mapper) throws IOException {
+        return mapper.readValue(data, AzureChatResult.class).buildResult();
+    }
+
+    @Override
+    protected ChatResult handleError(byte[] data, int statusCode, ObjectMapper mapper) {
+        String message = data != null ? new String(data, StandardCharsets.UTF_8) : null;
+        switch (statusCode) {
+        case 400:
+            throw new ChatRequestError(message != null ? message : "chat request error");
+        case 404:
+            throw new NotFoundException(message != null ? message : "chat resource not found");
+        default:
+            throw new StatusException(message != null ? message : "chat api error");
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        ChatApiConfig config = new ChatApiConfig();
+        config.setSupplier("Azure");
+        config.setUrl(
+                "https://qmth-ai.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2024-02-01");
+        config.setSecret("ed11ce6c22984fc085db7acdd023828b");
+        config.setModel("gpt35");
+        config.setQpm(0);
+        AzureChatClient client = new AzureChatClient(config);
+        ChatRequest request = new ChatRequest();
+        request.addMessage(ChatRole.user, "who are you");
+        System.out.println(new ObjectMapper().writeValueAsString(client.call(request)));
+    }
+}

+ 47 - 0
src/main/java/com/qmth/ops/biz/ai/client/azure/AzureChatResult.java

@@ -0,0 +1,47 @@
+package com.qmth.ops.biz.ai.client.azure;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.qmth.boot.core.ai.model.llm.ChatChoice;
+import com.qmth.boot.core.ai.model.llm.ChatResult;
+
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AzureChatResult {
+
+    private List<ChatChoice> choices;
+
+    private AzureUsage usage;
+
+    private String model;
+
+    public ChatResult buildResult() {
+        ChatResult result = new ChatResult();
+        result.setChoices(choices);
+        return result;
+    }
+
+    public List<ChatChoice> getChoices() {
+        return choices;
+    }
+
+    public void setChoices(List<ChatChoice> choices) {
+        this.choices = choices;
+    }
+
+    public AzureUsage getUsage() {
+        return usage;
+    }
+
+    public void setUsage(AzureUsage usage) {
+        this.usage = usage;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+}

+ 41 - 0
src/main/java/com/qmth/ops/biz/ai/client/azure/AzureUsage.java

@@ -0,0 +1,41 @@
+package com.qmth.ops.biz.ai.client.azure;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AzureUsage {
+
+    @JsonProperty("prompt_tokens")
+    private int PromptTokens;
+
+    @JsonProperty("completion_tokens")
+    private int completionTokens;
+
+    @JsonProperty("total_tokens")
+    private int totalTokens;
+
+    public int getPromptTokens() {
+        return PromptTokens;
+    }
+
+    public void setPromptTokens(int promptTokens) {
+        PromptTokens = promptTokens;
+    }
+
+    public int getCompletionTokens() {
+        return completionTokens;
+    }
+
+    public void setCompletionTokens(int completionTokens) {
+        this.completionTokens = completionTokens;
+    }
+
+    public int getTotalTokens() {
+        return totalTokens;
+    }
+
+    public void setTotalTokens(int totalTokens) {
+        this.totalTokens = totalTokens;
+    }
+}

+ 12 - 0
src/main/java/com/qmth/ops/biz/ai/exception/ChatClientNotFound.java

@@ -0,0 +1,12 @@
+package com.qmth.ops.biz.ai.exception;
+
+import com.qmth.boot.core.exception.NotFoundException;
+
+public class ChatClientNotFound extends NotFoundException {
+
+    private static final long serialVersionUID = -1899123546785881727L;
+
+    public ChatClientNotFound(Long modelId) {
+        super("Chat api client not found for model: " + modelId);
+    }
+}

+ 12 - 0
src/main/java/com/qmth/ops/biz/ai/exception/ChatRateLimitExceeded.java

@@ -0,0 +1,12 @@
+package com.qmth.ops.biz.ai.exception;
+
+import com.qmth.boot.core.exception.ReentrantException;
+
+public class ChatRateLimitExceeded extends ReentrantException {
+
+    private static final long serialVersionUID = -7994883363248623740L;
+
+    public ChatRateLimitExceeded(String supplier, String model, int qpm) {
+        super("Chat api rate limit exceeded, supplier=" + supplier + ", model=" + model + ", qpm=" + qpm);
+    }
+}

+ 12 - 0
src/main/java/com/qmth/ops/biz/ai/exception/ChatRequestError.java

@@ -0,0 +1,12 @@
+package com.qmth.ops.biz.ai.exception;
+
+import com.qmth.boot.core.exception.ParameterException;
+
+public class ChatRequestError extends ParameterException {
+
+    private static final long serialVersionUID = -631629102599173365L;
+
+    public ChatRequestError(String message) {
+        super(message);
+    }
+}

+ 8 - 0
src/main/java/com/qmth/ops/biz/dao/LlmModelDao.java

@@ -0,0 +1,8 @@
+package com.qmth.ops.biz.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qmth.ops.biz.domain.LlmModel;
+
+public interface LlmModelDao extends BaseMapper<LlmModel> {
+
+}

+ 17 - 0
src/main/java/com/qmth/ops/biz/dao/LlmOrgConfigDao.java

@@ -0,0 +1,17 @@
+package com.qmth.ops.biz.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qmth.boot.core.ai.model.llm.LlmAppType;
+import com.qmth.ops.biz.domain.LlmOrgConfig;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
+
+public interface LlmOrgConfigDao extends BaseMapper<LlmOrgConfig> {
+
+    @Update("update llm_org_config set left_count=left_count+#{count}, permit_count=permit_count+#{count}"
+            + " where org_id=#{orgId} and app_type=#{appType}")
+    void permit(@Param("orgId") Long orgId, @Param("appType") LlmAppType appType, @Param("count") int count);
+
+    @Update("update llm_org_config set left_count=left_count-1 where org_id=#{orgId} and app_type=#{appType} and left_count>0")
+    void consume(@Param("orgId") Long orgId, @Param("appType") LlmAppType appType);
+}

+ 8 - 0
src/main/java/com/qmth/ops/biz/dao/LlmSupplierDao.java

@@ -0,0 +1,8 @@
+package com.qmth.ops.biz.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.qmth.ops.biz.domain.LlmSupplier;
+
+public interface LlmSupplierDao extends BaseMapper<LlmSupplier> {
+
+}

+ 84 - 0
src/main/java/com/qmth/ops/biz/domain/LlmModel.java

@@ -0,0 +1,84 @@
+package com.qmth.ops.biz.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.io.Serializable;
+
+@TableName("llm_model")
+public class LlmModel implements Serializable {
+
+    private static final long serialVersionUID = -5964169705328617643L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long supplierId;
+
+    private String name;
+
+    private Integer qpm;
+
+    private Integer tpm;
+
+    private Long createTime;
+
+    private Long updateTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getSupplierId() {
+        return supplierId;
+    }
+
+    public void setSupplierId(Long supplierId) {
+        this.supplierId = supplierId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Integer getQpm() {
+        return qpm;
+    }
+
+    public void setQpm(Integer qpm) {
+        this.qpm = qpm;
+    }
+
+    public Integer getTpm() {
+        return tpm;
+    }
+
+    public void setTpm(Integer tpm) {
+        this.tpm = tpm;
+    }
+
+    public Long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Long createTime) {
+        this.createTime = createTime;
+    }
+
+    public Long getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Long updateTime) {
+        this.updateTime = updateTime;
+    }
+}

+ 62 - 0
src/main/java/com/qmth/ops/biz/domain/LlmOrgConfig.java

@@ -0,0 +1,62 @@
+package com.qmth.ops.biz.domain;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.qmth.boot.core.ai.model.llm.LlmAppType;
+
+import java.io.Serializable;
+
+@TableName("llm_org_config")
+public class LlmOrgConfig implements Serializable {
+
+    private static final long serialVersionUID = -593409647805304621L;
+
+    private Long orgId;
+
+    private LlmAppType appType;
+
+    private Long modelId;
+
+    private Integer permitCount;
+
+    private Integer leftCount;
+
+    public Long getOrgId() {
+        return orgId;
+    }
+
+    public void setOrgId(Long orgId) {
+        this.orgId = orgId;
+    }
+
+    public LlmAppType getAppType() {
+        return appType;
+    }
+
+    public void setAppType(LlmAppType appType) {
+        this.appType = appType;
+    }
+
+    public Long getModelId() {
+        return modelId;
+    }
+
+    public void setModelId(Long modelId) {
+        this.modelId = modelId;
+    }
+
+    public Integer getPermitCount() {
+        return permitCount;
+    }
+
+    public void setPermitCount(Integer permitCount) {
+        this.permitCount = permitCount;
+    }
+
+    public Integer getLeftCount() {
+        return leftCount;
+    }
+
+    public void setLeftCount(Integer leftCount) {
+        this.leftCount = leftCount;
+    }
+}

+ 84 - 0
src/main/java/com/qmth/ops/biz/domain/LlmSupplier.java

@@ -0,0 +1,84 @@
+package com.qmth.ops.biz.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.io.Serializable;
+
+@TableName("llm_supplier")
+public class LlmSupplier implements Serializable {
+
+    private static final long serialVersionUID = -9059099407152088825L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private String url;
+
+    private String secret;
+
+    private String chatClientClass;
+
+    private Long createTime;
+
+    private Long updateTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public String getChatClientClass() {
+        return chatClientClass;
+    }
+
+    public void setChatClientClass(String chatClientClass) {
+        this.chatClientClass = chatClientClass;
+    }
+
+    public Long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Long createTime) {
+        this.createTime = createTime;
+    }
+
+    public Long getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Long updateTime) {
+        this.updateTime = updateTime;
+    }
+}

+ 64 - 0
src/main/java/com/qmth/ops/biz/service/LlmClientService.java

@@ -0,0 +1,64 @@
+package com.qmth.ops.biz.service;
+
+import com.qmth.boot.core.ai.model.llm.ChatRequest;
+import com.qmth.boot.core.ai.model.llm.ChatResult;
+import com.qmth.ops.biz.ai.client.ChatApiClient;
+import com.qmth.ops.biz.ai.client.ChatApiConfig;
+import com.qmth.ops.biz.ai.exception.ChatClientNotFound;
+import com.qmth.ops.biz.domain.LlmModel;
+import com.qmth.ops.biz.domain.LlmSupplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class LlmClientService {
+
+    private static final Logger log = LoggerFactory.getLogger(LlmClientService.class);
+
+    @Resource
+    private LlmSupplierService supplierService;
+
+    @Resource
+    private LlmModelService modelService;
+
+    private Map<Long, ChatApiClient> chatApiClientMap = new HashMap<>();
+
+    @PostConstruct
+    public void init() {
+        List<LlmSupplier> supplierList = supplierService.list();
+        for (LlmSupplier supplier : supplierList) {
+            List<LlmModel> modelList = modelService.listBySupplier(supplier.getId());
+            for (LlmModel model : modelList) {
+                initChatApiClient(supplier, model);
+            }
+        }
+    }
+
+    private void initChatApiClient(LlmSupplier supplier, LlmModel model) {
+        try {
+            String className = ChatApiClient.class.getName().replace("ChatApiClient", supplier.getChatClientClass());
+            ChatApiConfig config = new ChatApiConfig(supplier, model);
+            Class<?> clientClass = Class.forName(className);
+            chatApiClientMap.put(model.getId(),
+                    (ChatApiClient) clientClass.getConstructor(ChatApiConfig.class).newInstance(config));
+        } catch (Exception e) {
+            log.error("Chat api client init error, supplier={}, class={}, model={}", supplier.getName(),
+                    supplier.getChatClientClass(), model.getName());
+        }
+    }
+
+    public ChatResult chat(ChatRequest request, Long modelId) throws Exception {
+        ChatApiClient client = chatApiClientMap.get(modelId);
+        if (client == null) {
+            throw new ChatClientNotFound(modelId);
+        }
+        return client.call(request);
+    }
+}

+ 41 - 0
src/main/java/com/qmth/ops/biz/service/LlmModelService.java

@@ -0,0 +1,41 @@
+package com.qmth.ops.biz.service;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.ops.biz.dao.LlmModelDao;
+import com.qmth.ops.biz.domain.LlmModel;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Service
+public class LlmModelService extends ServiceImpl<LlmModelDao, LlmModel> {
+
+    @Resource
+    private LlmModelDao modelDao;
+
+    @Transactional
+    public LlmModel insert(LlmModel model) {
+        model.setCreateTime(System.currentTimeMillis());
+        model.setUpdateTime(model.getCreateTime());
+        modelDao.insert(model);
+        return model;
+    }
+
+    @Transactional
+    public void update(LlmModel model) {
+        modelDao.update(model,
+                new LambdaUpdateWrapper<LlmModel>().set(model.getName() != null, LlmModel::getName, model.getName())
+                        .set(model.getQpm() != null, LlmModel::getQpm, model.getQpm())
+                        .set(model.getTpm() != null, LlmModel::getTpm, model.getTpm())
+                        .set(LlmModel::getUpdateTime, System.currentTimeMillis()).eq(LlmModel::getId, model.getId()));
+    }
+
+    public List<LlmModel> listBySupplier(Long supplierId) {
+        return modelDao.selectList(new LambdaUpdateWrapper<LlmModel>().eq(LlmModel::getSupplierId, supplierId));
+    }
+
+}
+

+ 46 - 0
src/main/java/com/qmth/ops/biz/service/LlmOrgConfigService.java

@@ -0,0 +1,46 @@
+package com.qmth.ops.biz.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.boot.core.ai.model.llm.LlmAppType;
+import com.qmth.ops.biz.dao.LlmOrgConfigDao;
+import com.qmth.ops.biz.domain.LlmModel;
+import com.qmth.ops.biz.domain.LlmOrgConfig;
+import com.qmth.ops.biz.domain.Org;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+
+@Service
+public class LlmOrgConfigService extends ServiceImpl<LlmOrgConfigDao, LlmOrgConfig> {
+
+    @Resource
+    private LlmOrgConfigDao llmOrgConfigDao;
+
+    public LlmOrgConfig findByOrgAndAppType(Org org, LlmAppType appType) {
+        return llmOrgConfigDao.selectOne(new LambdaQueryWrapper<LlmOrgConfig>().eq(LlmOrgConfig::getOrgId, org.getId())
+                .eq(LlmOrgConfig::getAppType, appType));
+    }
+
+    @Transactional
+    public void updateModel(LlmOrgConfig llmOrgConfig, LlmModel model) {
+        llmOrgConfigDao.update(llmOrgConfig,
+                new LambdaUpdateWrapper<LlmOrgConfig>().set(LlmOrgConfig::getModelId, model.getId())
+                        .eq(LlmOrgConfig::getOrgId, llmOrgConfig.getOrgId())
+                        .eq(LlmOrgConfig::getAppType, llmOrgConfig.getAppType()));
+    }
+
+    @Transactional
+    public void permit(LlmOrgConfig llmOrgConfig, int count) {
+        llmOrgConfigDao.permit(llmOrgConfig.getOrgId(), llmOrgConfig.getAppType(), count);
+    }
+
+    @Transactional
+    public void consume(LlmOrgConfig llmOrgConfig) {
+        llmOrgConfigDao.consume(llmOrgConfig.getOrgId(), llmOrgConfig.getAppType());
+    }
+
+}
+

+ 41 - 0
src/main/java/com/qmth/ops/biz/service/LlmSupplierService.java

@@ -0,0 +1,41 @@
+package com.qmth.ops.biz.service;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.qmth.ops.biz.dao.LlmSupplierDao;
+import com.qmth.ops.biz.domain.LlmSupplier;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Service
+public class LlmSupplierService extends ServiceImpl<LlmSupplierDao, LlmSupplier> {
+
+    @Resource
+    private LlmSupplierDao supplierDao;
+
+    @Transactional
+    public LlmSupplier insert(LlmSupplier supplier) {
+        supplier.setCreateTime(System.currentTimeMillis());
+        supplier.setUpdateTime(supplier.getCreateTime());
+        supplierDao.insert(supplier);
+        return supplier;
+    }
+
+    @Transactional
+    public void update(LlmSupplier supplier) {
+        supplierDao.update(supplier, new LambdaUpdateWrapper<LlmSupplier>()
+                .set(supplier.getName() != null, LlmSupplier::getName, supplier.getName())
+                .set(supplier.getUrl() != null, LlmSupplier::getUrl, supplier.getUrl())
+                .set(supplier.getSecret() != null, LlmSupplier::getSecret, supplier.getSecret())
+                .set(LlmSupplier::getUpdateTime, System.currentTimeMillis()).eq(LlmSupplier::getId, supplier.getId()));
+    }
+
+    public List<LlmSupplier> list() {
+        return supplierDao.selectList(new LambdaUpdateWrapper<>());
+    }
+
+}
+

+ 5 - 0
src/main/java/com/qmth/ops/biz/service/OrgService.java

@@ -1,5 +1,6 @@
 package com.qmth.ops.biz.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.qmth.ops.biz.dao.OrgDao;
 import com.qmth.ops.biz.domain.Org;
 import com.qmth.ops.biz.query.OrgQuery;
@@ -25,6 +26,10 @@ public class OrgService {
         return orgDao.findByQuery(query).getRecords();
     }
 
+    public Org findByAccessKey(String accessKey) {
+        return orgDao.selectOne(new LambdaQueryWrapper<Org>().eq(Org::getAccessKey, accessKey));
+    }
+
     @Transactional
     public boolean toggle(Long id, boolean enable) {
         return orgDao.toggle(id, enable) > 0;

+ 45 - 5
src/main/resources/script/init.sql

@@ -118,8 +118,8 @@ CREATE TABLE IF NOT EXISTS `nginx_config`
 
 CREATE TABLE IF NOT EXISTS `deploy`
 (
-    `id`            bigint(11) unsigned NOT NULL AUTO_INCREMENT,
-    `app_id`        bigint(11) unsigned NOT NULL,
+    `id`            bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+    `app_id`        bigint(20) unsigned NOT NULL,
     `name`          varchar(64)         NOT NULL,
     `mode`          varchar(16)         NOT NULL,
     `control`       text DEFAULT NULL,
@@ -154,7 +154,7 @@ CREATE TABLE IF NOT EXISTS `deploy_org`
 
 CREATE TABLE IF NOT EXISTS `org`
 (
-    `id`            bigint(11) unsigned NOT NULL AUTO_INCREMENT,
+    `id`            bigint(20) unsigned NOT NULL AUTO_INCREMENT,
     `code`          varchar(16)         NOT NULL,
     `name`          varchar(64)         NOT NULL,
     `type`          varchar(16)         NOT NULL,
@@ -187,8 +187,8 @@ CREATE TABLE IF NOT EXISTS `wxapp`
 
 CREATE TABLE IF NOT EXISTS `control_param`
 (
-    `id`     bigint(11) unsigned NOT NULL AUTO_INCREMENT,
-    `app_id` bigint(11) unsigned NOT NULL,
+    `id`     bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+    `app_id` bigint(20) unsigned NOT NULL,
     `key`    varchar(64)         NOT NULL,
     `name`   varchar(64)         NOT NULL,
     `type`   varchar(16)         NOT NULL,
@@ -196,3 +196,43 @@ CREATE TABLE IF NOT EXISTS `control_param`
     UNIQUE KEY `app_key` (`app_id`, `key`)
 ) ENGINE = InnoDB
   DEFAULT CHARSET = utf8mb4;
+
+
+CREATE TABLE IF NOT EXISTS `llm_supplier`
+(
+    `id`                bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+    `name`              varchar(32)         NOT NULL,
+    `url`               varchar(128)        NOT NULL,
+    `secret`            varchar(128)        NOT NULL,
+    `chat_client_class` varchar(128)        NOT NULL,
+    `create_time`       bigint(20)          NOT NULL,
+    `update_time`       bigint(20)          NOT NULL,
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4;
+
+
+CREATE TABLE IF NOT EXISTS `llm_model`
+(
+    `id`          bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+    `supplier_id` bigint(20) unsigned NOT NULL,
+    `name`        varchar(32)         NOT NULL,
+    `qpm`         int(11)             NOT NULL,
+    `tpm`         int(11)             NOT NULL,
+    `create_time` bigint(20)          NOT NULL,
+    `update_time` bigint(20)          NOT NULL,
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4;
+
+
+CREATE TABLE IF NOT EXISTS `llm_org_config`
+(
+    `org_id`       bigint(20) unsigned NOT NULL,
+    `app_type`     varchar(32)         NOT NULL,
+    `model_id`     bigint(20) unsigned NOT NULL,
+    `permit_count` int(11)             NOT NULL,
+    `left_count`   int(11)             NOT NULL,
+    PRIMARY KEY (`org_id`, `app_type`)
+) ENGINE = InnoDB
+  DEFAULT CHARSET = utf8mb4;