From 9c6cff33ad7331762bab918d04666e462b4093f3 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 12 Jul 2026 22:15:40 +0800 Subject: [PATCH] feat(ai): 优化大模型兼容接入 --- yudao-module-ai/pom.xml | 42 ++++++++++++++---------------------------- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java | 1 + yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java | 3 ++- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java | 16 +++++++++------- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java | 2 +- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java | 2 +- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java | 13 ++++--------- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java | 47 +++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java | 47 +++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java | 11 ----------- yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java | 60 ++++++++++++++++++++++++++++++++++++++++++------------------ yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java | 38 ++++++++++++++++++++++++++------------ yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java | 20 ++++++++++++++++---- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java | 34 +++++++++++++++++++++------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java | 83 ++++++++++++++++------------------------------------------------------------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java | 44 ++++++++++++++++++++++++++++++-------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java | 46 ++++++++++++++++++++++++++++++---------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java | 37 ++++++++++++++++++++----------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java | 46 ++++++++++++++++++++++++++++++---------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java | 45 ++++++++++++++++++++++++++++++--------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java | 10 ++++++++-- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java | 43 ------------------------------------------- yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java | 35 ----------------------------------- yudao-server/src/main/resources/application.yaml | 77 ++++++++++++++++++++++++++++++++++++++++++++++------------------------------- 33 files changed, 1169 insertions(+), 557 deletions(-) create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java create mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java create mode 100644 yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java create mode 100644 yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java delete mode 100644 yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java delete mode 100644 yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 9dba070..25947cf 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -19,12 +19,24 @@ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno - 1.1.5 + 1.1.8 1.1.2.2 1.2.6 + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + cn.iocoder.boot @@ -112,37 +124,11 @@ ${spring-ai.version} - - org.springframework.ai - spring-ai-starter-model-zhipuai - ${spring-ai.version} - - - org.springframework.ai - spring-ai-starter-model-minimax - ${spring-ai.version} - - - com.alibaba.cloud.ai spring-ai-alibaba-starter-dashscope ${alibaba-ai.version} - - - - org.springaicommunity - qianfan-spring-boot-starter - 1.0.0 - - - - org.springaicommunity - moonshot-spring-boot-starter - 1.0.0 - - @@ -260,4 +246,4 @@ - \ No newline at end of file + diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 8a8a388..4658a08 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -12,6 +12,7 @@ public interface ErrorCodeConstants { // ========== API 密钥 1-040-000-000 ========== ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在"); ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!"); + ErrorCode API_CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_040_000_002, "AI 配置({})无法解析,请检查环境变量或配置项"); // ========== API 模型 1-040-001-000 ========== ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java index bf15ec1..3c09144 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java @@ -28,6 +28,7 @@ public enum AiPlatformEnum implements ArrayValuable { MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技 MOONSHOT("Moonshot", "月之暗面"), // KIMI BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能 + STEP_FUN("StepFun", "阶跃星辰"), // 阶跃星辰 // ========== 国外平台 ========== @@ -40,7 +41,7 @@ public enum AiPlatformEnum implements ArrayValuable { STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney SUNO("Suno", "Suno"), // Suno AI - GROK("Grok","Grok"), // Grok + GROK("Grok", "Grok"), // Grok ; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index 9009cbc..3939f6c 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -10,10 +10,15 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; import cn.iocoder.yudao.module.ai.tool.method.PersonService; @@ -44,7 +49,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; -import java.util.Optional; /** * 芋道 AI 自动配置 @@ -113,13 +117,13 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(DouBaoChatModel.MODEL_DEFAULT); } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() .baseUrl(DouBaoChatModel.BASE_URL) .completionsPath(DouBaoChatModel.COMPLETE_PATH) .apiKey(properties.getApiKey()) .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) @@ -168,11 +172,8 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(HunYuanChatModel.MODEL_DEFAULT); } - // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL if (StrUtil.isEmpty(properties.getBaseUrl())) { - properties.setBaseUrl( - StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL - : HunYuanChatModel.BASE_URL); + properties.setBaseUrl(HunYuanChatModel.BASE_URL); } // 创建 DeepSeekChatModel、HunYuanChatModel 对象 DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() @@ -203,25 +204,15 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(XingHuoChatModel.MODEL_DEFAULT); } - OpenAiApi.Builder builder = OpenAiApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL_V1) - .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()); - if ("x1".equals(properties.getModel())) { - builder.baseUrl(XingHuoChatModel.BASE_URL_V2) - .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2); - } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(builder.build()) - .defaultOptions(OpenAiChatOptions.builder() + return XingHuoChatModel.builder() + .apiKey(properties.getApiKey()) + .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) - // TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!! - .toolCallingManager(getToolCallingManager()) .build(); - return new XingHuoChatModel(openAiChatModel); } @Bean @@ -235,12 +226,13 @@ public class AiAutoConfiguration { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() + DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() .baseUrl(BaiChuanChatModel.BASE_URL) .apiKey(properties.getApiKey()) + .completionsPath(BaiChuanChatModel.COMPLETE_PATH) .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) @@ -248,7 +240,123 @@ public class AiAutoConfiguration { .build()) .toolCallingManager(getToolCallingManager()) .build(); - return new BaiChuanChatModel(openAiChatModel); + return new BaiChuanChatModel(deepSeekChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true") + public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan(); + return buildYiYanChatClient(properties); + } + + public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(YiYanChatModel.MODEL_DEFAULT); + } + return new YiYanChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL), + null, properties.getApiKey(), properties.getModel(), properties.getTemperature(), + properties.getMaxTokens(), properties.getTopP())); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true") + public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu(); + return buildZhiPuChatClient(properties); + } + + public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(ZhiPuChatModel.MODEL_DEFAULT); + } + DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL)) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .build(); + return new ZhiPuChatModel(deepSeekChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true") + public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax(); + return buildMiniMaxChatClient(properties); + } + + public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(MiniMaxChatModel.MODEL_DEFAULT); + } + return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL), + null, properties.getApiKey(), properties.getModel(), properties.getTemperature(), + properties.getMaxTokens(), properties.getTopP())); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true") + public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot(); + return buildMoonshotChatClient(properties); + } + + public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(MoonshotChatModel.MODEL_DEFAULT); + } + return new MoonshotChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL), + MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(), + properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true") + public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun(); + return buildStepFunChatClient(properties); + } + + public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(StepFunChatModel.MODEL_DEFAULT); + } + return new StepFunChatModel(buildDeepSeekCompatibleChatModel( + StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL), + StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(), + properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); + } + + private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath, + String apiKey, String model, + Double temperature, Integer maxTokens, + Double topP) { + DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder() + .baseUrl(baseUrl) + .apiKey(apiKey); + if (StrUtil.isNotEmpty(completionsPath)) { + apiBuilder.completionsPath(completionsPath); + } + return DeepSeekChatModel.builder() + .deepSeekApi(apiBuilder.build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(model) + .temperature(temperature) + .maxTokens(maxTokens) + .topP(topP) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); } @Bean @@ -264,14 +372,20 @@ public class AiAutoConfiguration { return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); } - public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { + @Bean + @ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true") + public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok(); + return buildGrokChatClient(properties); + } + + public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(GrokChatModel.MODEL_DEFAULT); } OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() - .baseUrl(Optional.ofNullable(properties.getBaseUrl()) - .orElse(GrokChatModel.BASE_URL)) + .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL)) .completionsPath(GrokChatModel.COMPLETE_PATH) .apiKey(properties.getApiKey()) .build()) @@ -283,7 +397,7 @@ public class AiAutoConfiguration { .build()) .toolCallingManager(getToolCallingManager()) .build(); - return new DouBaoChatModel(openAiChatModel); + return new GrokChatModel(openAiChatModel); } // ========== RAG 相关 ========== @@ -320,4 +434,4 @@ public class AiAutoConfiguration { return List.of(ToolCallbacks.from(personService)); } -} \ No newline at end of file +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java index 986c24c..2551104 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java @@ -44,6 +44,36 @@ public class YudaoAiProperties { private BaiChuan baichuan; /** + * 文心一言 + */ + private YiYan yiyan; + + /** + * 智谱 + */ + private ZhiPu zhipu; + + /** + * MiniMax + */ + private MiniMax minimax; + + /** + * 月之暗面 + */ + private Moonshot moonshot; + + /** + * 阶跃星辰 + */ + private StepFun stepfun; + + /** + * Grok + */ + private Grok grok; + + /** * Midjourney 绘图 */ private Midjourney midjourney; @@ -116,9 +146,7 @@ public class YudaoAiProperties { public static class XingHuo { private String enable; - private String appId; - private String appKey; - private String secretKey; + private String apiKey; private String model; private Double temperature; @@ -141,6 +169,76 @@ public class YudaoAiProperties { } @Data + public static class YiYan { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class ZhiPu { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class MiniMax { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class Moonshot { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class StepFun { + + private String enable; + private String apiKey; + private String baseUrl; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data public static class Midjourney { private String enable; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java index f8067de..e5fb92c 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model; import cn.hutool.core.io.FileUtil; -import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Singleton; import cn.hutool.core.lang.func.Func0; import cn.hutool.core.util.ArrayUtil; @@ -15,14 +14,21 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; +import cn.iocoder.yudao.module.ai.util.AiUtils; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration; @@ -40,18 +46,6 @@ import io.milvus.client.MilvusServiceClient; import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import lombok.SneakyThrows; -import org.springaicommunity.moonshot.MoonshotChatModel; -import org.springaicommunity.moonshot.MoonshotChatOptions; -import org.springaicommunity.moonshot.api.MoonshotApi; -import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration; -import org.springaicommunity.qianfan.QianFanChatModel; -import org.springaicommunity.qianfan.QianFanEmbeddingModel; -import org.springaicommunity.qianfan.QianFanEmbeddingOptions; -import org.springaicommunity.qianfan.QianFanImageModel; -import org.springaicommunity.qianfan.api.QianFanApi; -import org.springaicommunity.qianfan.api.QianFanImageApi; -import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration; -import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.model.ChatModel; @@ -63,27 +57,17 @@ import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.image.ImageModel; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.minimax.MiniMaxEmbeddingModel; -import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; -import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration; -import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration; -import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration; import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration; import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; @@ -114,13 +98,9 @@ import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStore import org.springframework.ai.vectorstore.redis.RedisVectorStore; import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration; import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; -import org.springframework.ai.zhipuai.*; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.web.client.RestClient; import redis.clients.jedis.JedisPooled; import java.io.File; @@ -131,7 +111,6 @@ import java.util.Timer; import java.util.TimerTask; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; /** * AI Model 模型工厂的实现类 @@ -141,7 +120,9 @@ import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; public class AiModelFactoryImpl implements AiModelFactory { @Override - public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) { + public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { // noinspection EnhancedSwitchMigration @@ -164,6 +145,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildMiniMaxChatModel(apiKey, url); case MOONSHOT: return buildMoonshotChatModel(apiKey, url); + case STEP_FUN: + return buildStepFunChatModel(apiKey, url); case XING_HUO: return buildXingHuoChatModel(apiKey); case BAI_CHUAN: @@ -179,7 +162,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case OLLAMA: return buildOllamaChatModel(url); case GROK: - return buildGrokChatModel(apiKey,url); + return buildGrokChatModel(apiKey, url); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -193,7 +176,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case TONG_YI: return SpringUtil.getBean(DashScopeChatModel.class); case YI_YAN: - return SpringUtil.getBean(QianFanChatModel.class); + return SpringUtil.getBean(YiYanChatModel.class); case DEEP_SEEK: return SpringUtil.getBean(DeepSeekChatModel.class); case DOU_BAO: @@ -203,11 +186,13 @@ public class AiModelFactoryImpl implements AiModelFactory { case SILICON_FLOW: return SpringUtil.getBean(SiliconFlowChatModel.class); case ZHI_PU: - return SpringUtil.getBean(ZhiPuAiChatModel.class); + return SpringUtil.getBean(ZhiPuChatModel.class); case MINI_MAX: return SpringUtil.getBean(MiniMaxChatModel.class); case MOONSHOT: return SpringUtil.getBean(MoonshotChatModel.class); + case STEP_FUN: + return SpringUtil.getBean(StepFunChatModel.class); case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); case BAI_CHUAN: @@ -222,6 +207,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(GeminiChatModel.class); case OLLAMA: return SpringUtil.getBean(OllamaChatModel.class); + case GROK: + return SpringUtil.getBean(GrokChatModel.class); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -233,10 +220,6 @@ public class AiModelFactoryImpl implements AiModelFactory { switch (platform) { case TONG_YI: return SpringUtil.getBean(DashScopeImageModel.class); - case YI_YAN: - return SpringUtil.getBean(QianFanImageModel.class); - case ZHI_PU: - return SpringUtil.getBean(ZhiPuAiImageModel.class); case SILICON_FLOW: return SpringUtil.getBean(SiliconFlowImageModel.class); case OPENAI: @@ -249,19 +232,17 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { + public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String rawApiKey, String rawUrl) { + String apiKey = resolveSpringPlaceholders(rawApiKey); + String url = resolveSpringPlaceholders(rawUrl); // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return buildTongYiImagesModel(apiKey); - case YI_YAN: - return buildQianFanImageModel(apiKey); - case ZHI_PU: - return buildZhiPuAiImageModel(apiKey, url); case OPENAI: return buildOpenAiImageModel(apiKey, url); case SILICON_FLOW: - return buildSiliconFlowImageModel(apiKey,url); + return buildSiliconFlowImageModel(apiKey, url); case STABLE_DIFFUSION: return buildStabilityAiImageModel(apiKey, url); default: @@ -270,9 +251,11 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { - String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, - url); + public MidjourneyApi getOrCreateMidjourneyApi(String rawApiKey, String rawUrl) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), + apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class) .getMidjourney(); @@ -281,25 +264,23 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public SunoApi getOrCreateSunoApi(String apiKey, String url) { + public SunoApi getOrCreateSunoApi(String rawApiKey, String rawUrl) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url); return Singleton.get(cacheKey, (Func0) () -> new SunoApi(url)); } @Override @SuppressWarnings("EnhancedSwitchMigration") - public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) { + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String rawApiKey, String rawUrl, String model) { + final String apiKey = resolveSpringPlaceholders(rawApiKey); + final String url = resolveSpringPlaceholders(rawUrl); String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model); return Singleton.get(cacheKey, (Func0) () -> { switch (platform) { case TONG_YI: return buildTongYiEmbeddingModel(apiKey, model); - case YI_YAN: - return buildYiYanEmbeddingModel(apiKey, model); - case ZHI_PU: - return buildZhiPuEmbeddingModel(apiKey, url, model); - case MINI_MAX: - return buildMiniMaxEmbeddingModel(apiKey, url, model); case OPENAI: return buildOpenAiEmbeddingModel(apiKey, url, model); case AZURE_OPENAI: @@ -341,6 +322,11 @@ public class AiModelFactoryImpl implements AiModelFactory { return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); } + private static String resolveSpringPlaceholders(String value) { + // yml 配置的占位符由 Spring 自动解析;DB 里保存的 ${xxx} 需要在这里手动解析。 + return AiUtils.resolveSpringPlaceholders(value); + } + // ========== 各种创建 spring-ai 客户端的方法 ========== /** @@ -367,30 +353,10 @@ public class AiModelFactoryImpl implements AiModelFactory { .build(); } - /** - * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 - */ - private static QianFanChatModel buildYiYanChatModel(String key) { - // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); - return new QianFanChatModel(qianFanApi); - } - - /** - * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 - */ - private QianFanImageModel buildQianFanImageModel(String key) { - // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey); - return new QianFanImageModel(qianFanApi); + private ChatModel buildYiYanChatModel(String apiKey) { + YudaoAiProperties.YiYan properties = new YudaoAiProperties.YiYan() + .setApiKey(apiKey); + return new AiAutoConfiguration().buildYiYanChatClient(properties); } /** @@ -435,62 +401,47 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 + * 可参考 {@link AiAutoConfiguration#zhiPuChatClient(YudaoAiProperties)} */ - private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { - ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey); - if (StrUtil.isNotEmpty(url)) { - zhiPuAiApiBuilder.baseUrl(url); - } - ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new ZhiPuAiChatModel(zhiPuAiApiBuilder.build(), options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE, - getObservationRegistry().getIfAvailable()); + private ZhiPuChatModel buildZhiPuChatModel(String apiKey, String url) { + YudaoAiProperties.ZhiPu properties = new YudaoAiProperties.ZhiPu() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildZhiPuChatClient(properties); } /** - * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 + * 可参考 {@link AiAutoConfiguration#miniMaxChatClient(YudaoAiProperties)} */ - private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { - ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) - : new ZhiPuAiImageApi(url, apiKey, RestClient.builder()); - return new ZhiPuAiImageModel(zhiPuAiApi); + private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { + YudaoAiProperties.MiniMax properties = new YudaoAiProperties.MiniMax() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildMiniMaxChatClient(properties); } /** - * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 + * 可参考 {@link AiAutoConfiguration#moonshotChatClient(YudaoAiProperties)} */ - private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { - MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) - : new MiniMaxApi(url, apiKey); - MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); + private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { + YudaoAiProperties.Moonshot properties = new YudaoAiProperties.Moonshot() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildMoonshotChatClient(properties); } /** - * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 + * 可参考 {@link AiAutoConfiguration#stepFunChatClient(YudaoAiProperties)} */ - private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { - MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder() - .apiKey(apiKey); - if (StrUtil.isNotEmpty(url)) { - moonshotApiBuilder.baseUrl(url); - } - MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); - return MoonshotChatModel.builder() - .moonshotApi(moonshotApiBuilder.build()) - .defaultOptions(options) - .toolCallingManager(getToolCallingManager()) - .build(); + private StepFunChatModel buildStepFunChatModel(String apiKey, String url) { + YudaoAiProperties.StepFun properties = new YudaoAiProperties.StepFun() + .setBaseUrl(url).setApiKey(apiKey); + return new AiAutoConfiguration().buildStepFunChatClient(properties); } /** * 可参考 {@link AiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} */ - private static XingHuoChatModel buildXingHuoChatModel(String key) { - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); + private static XingHuoChatModel buildXingHuoChatModel(String apiKey) { YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo() - .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); + .setApiKey(apiKey).setModel(XingHuoChatModel.MODEL_DEFAULT); return new AiAutoConfiguration().buildXingHuoChatClient(properties); } @@ -590,7 +541,7 @@ public class AiModelFactoryImpl implements AiModelFactory { return new StabilityAiImageModel(stabilityAiApi); } - private ChatModel buildGrokChatModel(String apiKey,String url) { + private GrokChatModel buildGrokChatModel(String apiKey, String url) { YudaoAiProperties.Grok properties = new YudaoAiProperties.Grok() .setBaseUrl(url) .setApiKey(apiKey); @@ -608,41 +559,6 @@ public class AiModelFactoryImpl implements AiModelFactory { return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); } - /** - * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 - */ - private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { - ZhiPuAiApi.Builder zhiPuAiApiBuilder = ZhiPuAiApi.builder().apiKey(apiKey); - if (StrUtil.isNotEmpty(url)) { - zhiPuAiApiBuilder.baseUrl(url); - } - ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build(); - return new ZhiPuAiEmbeddingModel(zhiPuAiApiBuilder.build(), MetadataMode.EMBED, zhiPuAiEmbeddingOptions); - } - - /** - * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法 - */ - private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { - MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) - : new MiniMaxApi(url, apiKey); - MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build(); - return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions); - } - - /** - * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法 - */ - private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); - QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build(); - return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions); - } - private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaEmbeddingOptions ollamaOptions = OllamaEmbeddingOptions.builder().model(model).build(); diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java index 5fb71c9..7974194 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java @@ -6,7 +6,7 @@ import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.deepseek.DeepSeekChatModel; import reactor.core.publisher.Flux; /** @@ -20,26 +20,28 @@ public class BaiChuanChatModel implements ChatModel { public static final String BASE_URL = "https://api.baichuan-ai.com"; - public static final String MODEL_DEFAULT = "Baichuan4-Turbo"; + public static final String COMPLETE_PATH = "/v1/chat/completions"; + + public static final String MODEL_DEFAULT = "Baichuan-M3"; /** - * 兼容 OpenAI 接口,进行复用 + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 */ - private final OpenAiChatModel openAiChatModel; + private final DeepSeekChatModel deepSeekChatModel; @Override public ChatResponse call(Prompt prompt) { - return openAiChatModel.call(prompt); + return deepSeekChatModel.call(prompt); } @Override public Flux stream(Prompt prompt) { - return openAiChatModel.stream(prompt); + return deepSeekChatModel.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return openAiChatModel.getDefaultOptions(); + return deepSeekChatModel.getDefaultOptions(); } } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java index a542cb3..7b6e10c 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java @@ -20,7 +20,7 @@ public class DouBaoChatModel implements ChatModel { public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; public static final String COMPLETE_PATH = "/v3/chat/completions"; - public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115"; + public static final String MODEL_DEFAULT = "doubao-seed-2-1-turbo-260628"; /** * 兼容 OpenAI 接口,进行复用 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java index 06eed25..86c503e 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java @@ -19,7 +19,7 @@ public class GrokChatModel implements ChatModel { public static final String BASE_URL = "https://api.x.ai"; public static final String COMPLETE_PATH = "/v1/chat/completions"; - public static final String MODEL_DEFAULT = "grok-4-fast-reasoning"; + public static final String MODEL_DEFAULT = "grok-4.3"; /** * 兼容 OpenAI 接口,进行复用 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java index 9513c6c..6711ef2 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java @@ -9,10 +9,9 @@ import org.springframework.ai.chat.prompt.Prompt; import reactor.core.publisher.Flux; /** - * 腾云混元 {@link ChatModel} 实现类 + * 腾讯混元 {@link ChatModel} 实现类 * - * 1. 混元大模型:基于 知识引擎原子能力 实现 - * 2. 知识引擎原子能力:基于 知识引擎原子能力 实现 + * 基于 TokenHub OpenAI 兼容接口 实现 * * @author fansili */ @@ -20,14 +19,10 @@ import reactor.core.publisher.Flux; @RequiredArgsConstructor public class HunYuanChatModel implements ChatModel { - public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; + public static final String BASE_URL = "https://tokenhub.tencentmaas.com"; public static final String COMPLETE_PATH = "/v1/chat/completions"; - public static final String MODEL_DEFAULT = "hunyuan-turbo"; - - public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com"; - - public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3"; + public static final String MODEL_DEFAULT = "hy3-preview"; /** * 兼容 OpenAI 接口,进行复用 diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java new file mode 100644 index 0000000..d421aab --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * MiniMax {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class MiniMaxChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.minimaxi.com/v1"; + + public static final String MODEL_DEFAULT = "MiniMax-M3"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java new file mode 100644 index 0000000..27287e3 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 月之暗面 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class MoonshotChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.moonshot.cn"; + + public static final String COMPLETE_PATH = "/v1/chat/completions"; + + public static final String MODEL_DEFAULT = "kimi-k2.6"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java new file mode 100644 index 0000000..7d61141 --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 阶跃星辰 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class StepFunChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.stepfun.com"; + + public static final String COMPLETE_PATH = "/v1/chat/completions"; + + public static final String MODEL_DEFAULT = "step-3.7-flash"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java index cbac3b6..e7e4368 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java @@ -1,11 +1,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo; -import lombok.RequiredArgsConstructor; +import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; /** @@ -14,37 +17,140 @@ import reactor.core.publisher.Flux; * @author fansili */ @Slf4j -@RequiredArgsConstructor public class XingHuoChatModel implements ChatModel { - public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com"; - - public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com"; - public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions"; + /** + * 星火 X2 + * + * @see 接口地址 + */ + public static final String MODEL_X2 = "x2"; /** - * 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite + * 星火 X2 Flash + * + * @see 接口地址 */ - public static final String MODEL_DEFAULT = "4.0Ultra"; + public static final String MODEL_X2_FLASH = "x2-flash"; + + private static final String BASE_URL_X2 = "https://spark-api-open.xf-yun.com/x2"; + + private static final String BASE_URL_X2_FLASH = "https://spark-api-open.xf-yun.com/agent/v1"; + + public static final String MODEL_DEFAULT = MODEL_X2_FLASH; + + private static String getBaseUrl(String model) { + if (MODEL_X2_FLASH.equals(model)) { + return BASE_URL_X2_FLASH; + } + return BASE_URL_X2; + } + + private final String apiKey; + + private final DeepSeekChatOptions options; /** - * v1 兼容 OpenAI 接口,进行复用 + * 兼容 OpenAI 接口,进行复用 */ - private final ChatModel openAiChatModelV1; + private final ChatModel chatModelX2; + + private final ChatModel chatModelX2Flash; + + private XingHuoChatModel(String apiKey, DeepSeekChatOptions options) { + this.apiKey = apiKey; + this.options = options; + this.chatModelX2 = buildChatModel(MODEL_X2); + this.chatModelX2Flash = buildChatModel(MODEL_X2_FLASH); + } + + public static Builder builder() { + return new Builder(); + } @Override public ChatResponse call(Prompt prompt) { - return openAiChatModelV1.call(prompt); + return getChatModel(prompt).call(buildApiPrompt(prompt)); } @Override public Flux stream(Prompt prompt) { - return openAiChatModelV1.stream(prompt); + return getChatModel(prompt).stream(buildApiPrompt(prompt)); } @Override public ChatOptions getDefaultOptions() { - return openAiChatModelV1.getDefaultOptions(); + return options; + } + + private ChatModel getChatModel(Prompt prompt) { + String model = options.getModel(); + ChatOptions options = prompt.getOptions(); + if (options != null && isBusinessModel(options.getModel())) { + model = options.getModel(); + } + return getChatModel(model); + } + + private ChatModel getChatModel(String model) { + if (MODEL_X2_FLASH.equals(model)) { + return chatModelX2Flash; + } + return chatModelX2; + } + + private ChatModel buildChatModel(String model) { + return DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(getBaseUrl(model)) + .apiKey(apiKey) + .build()) + .defaultOptions(new DeepSeekChatOptions.Builder(options).model("spark-x").build()) + .build(); + } + + private static Prompt buildApiPrompt(Prompt prompt) { + ChatOptions options = prompt.getOptions(); + if (options == null) { + return prompt; + } + if (!(options instanceof DeepSeekChatOptions)) { + return prompt; + } + return Prompt.builder() + .messages(prompt.getInstructions()) + .chatOptions(new DeepSeekChatOptions.Builder((DeepSeekChatOptions) options).model("spark-x").build()) + .build(); + } + + private static boolean isBusinessModel(String model) { + return MODEL_X2.equals(model) || MODEL_X2_FLASH.equals(model); + } + + public static final class Builder { + + private String apiKey; + + private DeepSeekChatOptions options; + + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public Builder options(DeepSeekChatOptions options) { + this.options = options; + return this; + } + + public XingHuoChatModel build() { + DeepSeekChatOptions options = this.options != null ? this.options : DeepSeekChatOptions.builder().build(); + if (StrUtil.isEmpty(options.getModel())) { + options = new DeepSeekChatOptions.Builder(options).model(MODEL_DEFAULT).build(); + } + return new XingHuoChatModel(apiKey, options); + } + } } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java new file mode 100644 index 0000000..876d6bd --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 文心一言 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class YiYanChatModel implements ChatModel { + + public static final String BASE_URL = "https://qianfan.baidubce.com/v2"; + + public static final String MODEL_DEFAULT = "ernie-5.1"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java new file mode 100644 index 0000000..858f22c --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import reactor.core.publisher.Flux; + +/** + * 智谱 {@link ChatModel} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class ZhiPuChatModel implements ChatModel { + + public static final String BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; + + public static final String MODEL_DEFAULT = "glm-5.2"; + + /** + * 兼容 OpenAI 接口,复用 DeepSeek 客户端 + */ + private final DeepSeekChatModel deepSeekChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return deepSeekChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return deepSeekChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return deepSeekChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 3def2a9..0163edb 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -29,14 +29,12 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springaicommunity.qianfan.QianFanImageOptions; import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -168,15 +166,6 @@ public class AiImageServiceImpl implements AiImageService { .model(model.getModel()).n(1) .height(draw.getHeight()).width(draw.getWidth()) .build(); - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { - return QianFanImageOptions.builder() - .model(model.getModel()).N(1) - .height(draw.getHeight()).width(draw.getWidth()) - .build(); - } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { - return ZhiPuAiImageOptions.builder() - .model(model.getModel()) - .build(); } throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform()); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java index 1518523..95fe446 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java @@ -8,8 +8,7 @@ import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; -import org.springaicommunity.moonshot.MoonshotChatOptions; -import org.springaicommunity.qianfan.QianFanChatOptions; +import cn.hutool.extra.spring.SpringUtil; import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; @@ -17,14 +16,16 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.deepseek.DeepSeekAssistantMessage; import org.springframework.ai.deepseek.DeepSeekChatOptions; -import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import org.springframework.core.env.Environment; import java.util.*; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED; + /** * Spring AI 工具类 * @@ -36,13 +37,42 @@ public class AiUtils { public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID"; /** + * 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY} + * + * @param value 待解析的配置值 + * @return 解析后的配置值 + */ + public static String resolveSpringPlaceholders(String value) { + if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) { + return value; + } + try { + return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value); + } catch (IllegalArgumentException ex) { + throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value); + } + } + + /** + * 校验 API Key,避免集成测试使用默认占位值发起调用。 + * + * @param apiKey API Key + */ + public static void validateApiKey(String apiKey) { + if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) { + throw new IllegalStateException("apiKey 不能为空"); + } + } + + /** * 通义千问支持多模态的模型 * * @see 模型广场 * @see 必须开启 withMultiModel 参数 */ public static final Set TONG_YI_MULTI_MODELS = SetUtils.asSet( - // qwen3.5 / 3.6 系列(统一多模态主干) + // qwen3.5 / 3.6 / 3.7 系列(统一多模态主干) + "qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash", "qwen3.6-plus", "qwen3.6-flash", "qwen3.5-plus", "qwen3.5-flash", // qwen-vl 视觉理解 @@ -72,27 +102,21 @@ public class AiUtils { .enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 .multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型 .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case YI_YAN: - return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case DEEP_SEEK: case DOU_BAO: // 复用 DeepSeek 客户端 case HUN_YUAN: // 复用 DeepSeek 客户端 case SILICON_FLOW: // 复用 DeepSeek 客户端 + case YI_YAN: // 复用 DeepSeek 客户端 + case ZHI_PU: // 复用 DeepSeek 客户端 case XING_HUO: // 复用 DeepSeek 客户端 + case MINI_MAX: // 复用 DeepSeek 客户端 + case MOONSHOT: // 复用 DeepSeek 客户端 + case BAI_CHUAN: // 复用 DeepSeek 客户端 + case STEP_FUN: // 复用 DeepSeek 客户端 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case ZHI_PU: - return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case MINI_MAX: - return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); - case MOONSHOT: - return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case OPENAI: case GEMINI: // 复用 OpenAI 客户端 - case BAI_CHUAN: // 复用 OpenAI 客户端 case GROK: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); @@ -159,4 +183,4 @@ public class AiUtils { return MapUtil.getStr(output.getMetadata(), "reasoningContent"); } -} \ No newline at end of file +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java index 06b0b25..dea9023 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -8,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link BaiChuanChatModel} 集成测试 @@ -23,22 +27,27 @@ import java.util.List; */ public class BaiChuanChatModelTests { - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() + private static final String API_KEY = SystemUtil.get("BAICHUAN_API_KEY", + "sk-xxxx"); // 按需改成你的百川 API Key + private static final String MODEL = SystemUtil.get("BAICHUAN_MODEL", + BaiChuanChatModel.MODEL_DEFAULT); + + private final BaiChuanChatModel chatModel = new BaiChuanChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() .baseUrl(BaiChuanChatModel.BASE_URL) - .apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey + .completionsPath(BaiChuanChatModel.COMPLETE_PATH) + .apiKey(API_KEY) .build()) - .defaultOptions(OpenAiChatOptions.builder() - .model("Baichuan4-Turbo") // 模型(https://platform.baichuan-ai.com/docs/api) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) .temperature(0.7) .build()) - .build(); - - private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -48,11 +57,13 @@ public class BaiChuanChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -61,7 +72,10 @@ public class BaiChuanChatModelTests { // 调用 Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 - flux.doOnNext(System.out::println).then().block(); + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java index 2fbe0ee..64a28f8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -14,6 +15,9 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link DeepSeekChatModel} 集成测试 @@ -22,12 +26,17 @@ import java.util.List; */ public class DeepSeekChatModelTests { + private static final String API_KEY = SystemUtil.get("DEEPSEEK_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("DEEPSEEK_MODEL", + "deepseek-v4-flash"); + private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() - .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey + .apiKey(API_KEY) // apiKey .build()) .defaultOptions(DeepSeekChatOptions.builder() - .model("deepseek-chat") // 模型 + .model(MODEL) // 模型 .temperature(0.7) .build()) .build(); @@ -35,6 +44,7 @@ public class DeepSeekChatModelTests { @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -43,12 +53,13 @@ public class DeepSeekChatModelTests { // 调用 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 - System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -63,11 +74,12 @@ public class DeepSeekChatModelTests { @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("deepseek-reasoner") + .model(MODEL) .build(); // 调用 diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java index 38c4f0b..5ab3da4 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -15,6 +16,9 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link DouBaoChatModel} 集成测试 @@ -23,19 +27,19 @@ import java.util.List; */ public class DouBaoChatModelTests { - /** - * 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力! - */ + private static final String API_KEY = SystemUtil.get("DOUBAO_API_KEY", + "sk-xxxx"); // 按需改成你的豆包 API Key + private static final String MODEL = SystemUtil.get("DOUBAO_MODEL", + DouBaoChatModel.MODEL_DEFAULT); + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(DouBaoChatModel.BASE_URL) .completionsPath(DouBaoChatModel.COMPLETE_PATH) - .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey + .apiKey(API_KEY) .build()) .defaultOptions(DeepSeekChatOptions.builder() - .model("doubao-1-5-lite-32k-250115") // 模型(doubao) -// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao) -// .model("deepseek-r1-250120") // 模型(deepseek) + .model(MODEL) .temperature(0.7) .build()) .build(); @@ -45,6 +49,7 @@ public class DouBaoChatModelTests { @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -54,11 +59,13 @@ public class DouBaoChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -67,25 +74,26 @@ public class DouBaoChatModelTests { // 调用 Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 - flux.doOnNext(System.out::println).then().block(); + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); } @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("doubao-seed-1-6-thinking-250715") - .build(); // 调用 - Flux flux = chatModel.stream(new Prompt(messages, options)); + Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java new file mode 100644 index 0000000..9443def --- /dev/null +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java @@ -0,0 +1,81 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; + +/** + * {@link GrokChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class GrokChatModelTests { + + private static final String API_KEY = SystemUtil.get("GROK_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("GROK_MODEL", + GrokChatModel.MODEL_DEFAULT); + + private final GrokChatModel chatModel = new GrokChatModel(OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(GrokChatModel.BASE_URL) + .completionsPath(GrokChatModel.COMPLETE_PATH) + .apiKey(API_KEY) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(MODEL) + .temperature(0.7) + .build()) + .build()); + + @Test + @Disabled + public void testCall() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + } + + @Test + @Disabled + public void testStream() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java index eeafef2..2543fce 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -15,6 +16,9 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link HunYuanChatModel} 集成测试 @@ -23,14 +27,19 @@ import java.util.List; */ public class HunYuanChatModelTests { + private static final String API_KEY = SystemUtil.get("HUNYUAN_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("HUNYUAN_MODEL", + HunYuanChatModel.MODEL_DEFAULT); + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(HunYuanChatModel.BASE_URL) .completionsPath(HunYuanChatModel.COMPLETE_PATH) - .apiKey("sk-abc") // apiKey + .apiKey(API_KEY) // apiKey .build()) .defaultOptions(DeepSeekChatOptions.builder() - .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 + .model(MODEL) // 模型 .temperature(0.7) .build()) .build(); @@ -40,6 +49,7 @@ public class HunYuanChatModelTests { @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -48,12 +58,13 @@ public class HunYuanChatModelTests { // 调用 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 - System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -68,12 +79,12 @@ public class HunYuanChatModelTests { @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("hunyuan-a13b") -// .model("hunyuan-turbos-latest") + .model(MODEL) .build(); // 调用 @@ -85,66 +96,4 @@ public class HunYuanChatModelTests { }).then().block(); } - private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder() - .deepSeekApi(DeepSeekApi.builder() - .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) - .completionsPath(HunYuanChatModel.COMPLETE_PATH) - .apiKey("sk-abc") // apiKey - .build()) - .defaultOptions(DeepSeekChatOptions.builder() -// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3") - .model("deepseek-r1") // 模型("deepseek-r1") - .temperature(0.7) - .build()) - .build(); - - private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel); - - @Test - @Disabled - public void testCall_deepseek() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = deepSeekChatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - } - - @Test - @Disabled - public void testStream_deepseek() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = deepSeekChatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(System.out::println).then().block(); - } - - @Test - @Disabled - public void testStream_deepseek_thinking() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("deepseek-r1") - .build(); - - // 调用 - Flux flux = deepSeekChatModel.stream(new Prompt(messages, options)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } - } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java index 8fb133d..45615c8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -7,13 +9,16 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.minimax.api.MiniMaxApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link MiniMaxChatModel} 的集成测试 @@ -22,14 +27,24 @@ import java.util.List; */ public class MiniMaxChatModelTests { - private final MiniMaxChatModel chatModel = new MiniMaxChatModel( - new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥 - MiniMaxChatOptions.builder() - .model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型 - .build()); + private static final String API_KEY = SystemUtil.get("MINIMAX_API_KEY", + "sk-xxxx"); // 按需改成你的 MiniMax API Key + private static final String MODEL = SystemUtil.get("MINIMAX_MODEL", + MiniMaxChatModel.MODEL_DEFAULT); + + private final MiniMaxChatModel chatModel = new MiniMaxChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(MiniMaxChatModel.BASE_URL) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .build()) + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -39,12 +54,13 @@ public class MiniMaxChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -55,19 +71,19 @@ public class MiniMaxChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - MiniMaxChatOptions options = MiniMaxChatOptions.builder() - .model("MiniMax-M1") + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(MODEL) .build(); // 调用 @@ -75,7 +91,7 @@ public class MiniMaxChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java index b50ab80..9aaed7c 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java @@ -1,19 +1,24 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springaicommunity.moonshot.MoonshotChatModel; -import org.springaicommunity.moonshot.MoonshotChatOptions; -import org.springaicommunity.moonshot.api.MoonshotApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link MoonshotChatModel} 的集成测试 @@ -22,18 +27,27 @@ import java.util.List; */ public class MoonshotChatModelTests { - private final MoonshotChatModel chatModel = MoonshotChatModel.builder() - .moonshotApi(MoonshotApi.builder() - .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 + private static final String API_KEY = SystemUtil.get("MOONSHOT_API_KEY", + "sk-xxxx"); // 按需改成你的 Moonshot API Key + private static final String MODEL = SystemUtil.get("MOONSHOT_MODEL", + MoonshotChatModel.MODEL_DEFAULT); + + private final MoonshotChatModel chatModel = new MoonshotChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(MoonshotChatModel.BASE_URL) + .completionsPath(MoonshotChatModel.COMPLETE_PATH) + .apiKey(API_KEY) .build()) - .defaultOptions(MoonshotChatOptions.builder() - .model("kimi-k2-0711-preview") // 模型 + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .temperature(1D) .build()) - .build(); + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -43,12 +57,13 @@ public class MoonshotChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -59,20 +74,19 @@ public class MoonshotChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - MoonshotChatOptions options = MoonshotChatOptions.builder() -// .model("kimi-k2-0711-preview") - .model("kimi-thinking-preview") + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(MODEL) .build(); // 调用 @@ -80,7 +94,7 @@ public class MoonshotChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java new file mode 100644 index 0000000..1a6c47d --- /dev/null +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; + +/** + * {@link StepFunChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class StepFunChatModelTests { + + private static final String API_KEY = SystemUtil.get("STEPFUN_API_KEY", + "sk-xxxx"); + private static final String MODEL = SystemUtil.get("STEPFUN_MODEL", + StepFunChatModel.MODEL_DEFAULT); + + private final StepFunChatModel chatModel = new StepFunChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(StepFunChatModel.BASE_URL) + .completionsPath(StepFunChatModel.COMPLETE_PATH) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .temperature(0.7) + .build()) + .build()); + + @Test + @Disabled + public void testCall() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + } + + @Test + @Disabled + public void testStream() { + validateApiKey(API_KEY); + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); +// System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + System.out.println(response.getResult() != null ? response.getResult().getOutput() : null); + }).then().block(); + } + +} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java index 77dbd2b..2e08cf6 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -8,13 +9,14 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; -import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** * {@link XingHuoChatModel} 集成测试 @@ -23,24 +25,23 @@ import java.util.List; */ public class XingHuoChatModelTests { - private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() - .deepSeekApi(DeepSeekApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL_V2) - .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2) - .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey - .build()) - .defaultOptions(DeepSeekChatOptions.builder() -// .model("generalv3.5") // 模型 - .model("x1") // 模型 - .temperature(0.7) - .build()) - .build(); + private static final String API_KEY = SystemUtil.get("XINGHUO_API_KEY", + "sk-xxxx"); // 按需改成你的讯飞星火 API Key + private static final String MODEL = SystemUtil.get("XINGHUO_MODEL", + XingHuoChatModel.MODEL_DEFAULT); - private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); + private final XingHuoChatModel chatModel = XingHuoChatModel.builder() + .apiKey(API_KEY) + .options(DeepSeekChatOptions.builder() + .model(MODEL) + .temperature(0.7) + .build()) + .build(); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -55,6 +56,7 @@ public class XingHuoChatModelTests { @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -69,11 +71,12 @@ public class XingHuoChatModelTests { @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model("x1") + .model(MODEL) .build(); // 调用 @@ -81,7 +84,7 @@ public class XingHuoChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java index cb7be2a..72086de 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java @@ -1,62 +1,76 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springaicommunity.qianfan.QianFanChatModel; -import org.springaicommunity.qianfan.QianFanChatOptions; -import org.springaicommunity.qianfan.api.QianFanApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; -// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 /** - * {@link QianFanChatModel} 的集成测试 + * {@link YiYanChatModel} 的集成测试 * * @author fansili */ public class YiYanChatModelTests { - private final QianFanChatModel chatModel = new QianFanChatModel( - new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥 - QianFanChatOptions.builder() - .model("ERNIE-4.5-8K-Preview") - .build() - ); + private static final String API_KEY = SystemUtil.get("YIYAN_API_KEY", + "sk-xxxx"); // 按需改成你的文心一言 API Key + private static final String MODEL = SystemUtil.get("YIYAN_MODEL", + YiYanChatModel.MODEL_DEFAULT); + + private final YiYanChatModel chatModel = new YiYanChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(YiYanChatModel.BASE_URL) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) + .build()) + .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); - // TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名! -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new UserMessage("1 + 1 = ?")); // 调用 ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); - // TODO @芋艿:文心一言,只要带上 system message 就报错,已经各种测试,很莫名! -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new UserMessage("1 + 1 = ?")); // 调用 Flux flux = chatModel.stream(new Prompt(messages)); // 打印结果 - flux.doOnNext(System.out::println).then().block(); + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); + }).then().block(); } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java index 18c3179..290d19d 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -7,31 +9,43 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; /** - * {@link ZhiPuAiChatModel} 的集成测试 + * {@link ZhiPuChatModel} 的集成测试 * * @author 芋道源码 */ public class ZhiPuAiChatModelTests { - private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( - ZhiPuAiApi.builder().apiKey("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR").build(), // 密钥 - ZhiPuAiChatOptions.builder() - .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 + private static final String API_KEY = SystemUtil.get("ZHIPU_API_KEY", + "sk-xxxx"); // 按需改成你的智谱 API Key + private static final String MODEL = SystemUtil.get("ZHIPU_MODEL", + ZhiPuChatModel.MODEL_DEFAULT); + + private final ZhiPuChatModel chatModel = new ZhiPuChatModel(DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .baseUrl(ZhiPuChatModel.BASE_URL) + .apiKey(API_KEY) + .build()) + .defaultOptions(DeepSeekChatOptions.builder() + .model(MODEL) .build() - ); + ).build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -41,12 +55,13 @@ public class ZhiPuAiChatModelTests { ChatResponse response = chatModel.call(new Prompt(messages)); // 打印结果 System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); } @Test @Disabled public void testStream() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); @@ -57,19 +72,19 @@ public class ZhiPuAiChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } - // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 @Test @Disabled public void testStream_thinking() { + validateApiKey(API_KEY); // 准备参数 List messages = new ArrayList<>(); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); - ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder() - .model("GLM-4.5") + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(MODEL) .build(); // 调用 @@ -77,7 +92,7 @@ public class ZhiPuAiChatModelTests { // 打印结果 flux.doOnNext(response -> { // System.out.println(response); - System.out.println(response.getResult().getOutput()); + System.out.println(Objects.requireNonNull(response.getResult()).getOutput()); }).then().block(); } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java index 1b12452..7d53fe6 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; +import cn.hutool.system.SystemUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; @@ -9,6 +10,8 @@ import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.openai.api.OpenAiImageApi; +import static cn.iocoder.yudao.module.ai.util.AiUtils.validateApiKey; + /** * {@link OpenAiImageModel} 集成测试类 * @@ -16,17 +19,20 @@ import org.springframework.ai.openai.api.OpenAiImageApi; */ public class OpenAiImageModelTests { + private static final String API_KEY = SystemUtil.get("OPENAI_API_KEY", "sk-xxxx"); + private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() .baseUrl("https://api.holdai.top") // apiKey - .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") + .apiKey(API_KEY) .build()); @Test @Disabled public void testCall() { + validateApiKey(API_KEY); // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 + .model("gpt-image-1") .height(256).width(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java deleted file mode 100644 index 156360f..0000000 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springaicommunity.qianfan.QianFanImageModel; -import org.springaicommunity.qianfan.QianFanImageOptions; -import org.springaicommunity.qianfan.api.QianFanImageApi; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; - -import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage; - -// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 - -/** - * {@link QianFanImageModel} 集成测试类 - */ -public class QianFanImageTests { - - private final QianFanImageModel imageModel = new QianFanImageModel( - new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥 - - @Test - @Disabled - public void testCall() { - // 准备参数 - // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576 - QianFanImageOptions imageOptions = QianFanImageOptions.builder() - .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) - .width(1024).height(1024) - .N(1) - .build(); - ImagePrompt prompt = new ImagePrompt("good", imageOptions); - - // 方法调用 - ImageResponse response = imageModel.call(prompt); - // 打印结果 - String b64Json = response.getResult().getOutput().getB64Json(); - System.out.println(response); - viewImage(b64Json); - } - -} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java deleted file mode 100644 index bb16aac..0000000 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/ZhiPuAiImageModelTests.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; - -/** - * {@link ZhiPuAiImageModel} 集成测试 - */ -public class ZhiPuAiImageModelTests { - - private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel( - new ZhiPuAiImageApi("78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy") // 密钥 - ); - - @Test - @Disabled - public void testCall() { - // 准备参数 - ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder() - .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) - .build(); - ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions); - - // 方法调用 - ImageResponse response = imageModel.call(prompt); - // 打印结果 - System.out.println(response); - } - -} diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index de6f6d6..e51ee22 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -164,36 +164,28 @@ spring: client: host: 127.0.0.1 port: 19530 - qianfan: # 文心一言 - api-key: x0cuLZ7XsaTCU08vuJWO87Lg - secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK - zhipuai: # 智谱 AI - api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs openai: # OpenAI 官方 - api-key: sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17 - base-url: https://api.gptsapi.net + base-url: ${OPENAI_BASE_URL:https://api.openai.com} + api-key: ${OPENAI_API_KEY:sk-xxxx} azure: # OpenAI 微软 openai: endpoint: https://eastusprejade.openai.azure.com anthropic: # Anthropic Claude - api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942 + base-url: ${ANTHROPIC_BASE_URL:https://api.anthropic.com} + api-key: ${ANTHROPIC_API_KEY:sk-xxxx} ollama: base-url: http://127.0.0.1:11434 chat: model: llama3 stabilityai: - api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx + api-key: ${STABILITYAI_API_KEY:sk-xxxx} dashscope: # 通义千问 - api-key: sk-47aa124781be4bfb95244cc62f6xxxx - minimax: # Minimax:https://www.minimaxi.com/ - api-key: xxxx - moonshot: # 月之暗面(KIMI) - api-key: sk-abc + api-key: ${DASHSCOPE_API_KEY:sk-xxxx} deepseek: # DeepSeek - api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + api-key: ${DEEPSEEK_API_KEY:sk-xxxx} chat: options: - model: deepseek-chat + model: deepseek-v4-flash model: rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 mcp: @@ -218,34 +210,57 @@ yudao: ai: gemini: # 谷歌 Gemini enable: true - api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ + api-key: ${GEMINI_API_KEY:sk-xxxx} model: gemini-2.5-flash doubao: # 字节豆包 enable: true - api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 - model: doubao-1-5-lite-32k-250115 + api-key: ${DOUBAO_API_KEY:sk-xxxx} + model: doubao-seed-2-1-turbo-260628 hunyuan: # 腾讯混元 enable: true - api-key: sk-abc - model: hunyuan-turbo + api-key: ${HUNYUAN_API_KEY:sk-xxxx} + model: hy3-preview siliconflow: # 硅基流动 enable: true - api-key: sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz - model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B + api-key: ${SILICONFLOW_API_KEY:sk-xxxx} + model: deepseek-ai/DeepSeek-V4-Pro xinghuo: # 讯飞星火 enable: true - appKey: 75b161ed2aef4719b275d6e7f2a4d4cd - secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz - model: x1 + api-key: ${XINGHUO_API_KEY:sk-xxxx} + model: x2-flash baichuan: # 百川智能 enable: true - api-key: sk-abc - model: Baichuan4-Turbo + api-key: ${BAICHUAN_API_KEY:sk-xxxx} + model: Baichuan-M3 + yiyan: # 文心一言 + enable: false + api-key: ${YIYAN_API_KEY:sk-xxxx} + model: ernie-5.1 + zhipu: # 智谱 AI(GLM) + enable: false + api-key: ${ZHIPU_API_KEY:sk-xxxx} + model: glm-5.2 + minimax: # MiniMax + enable: false + api-key: ${MINIMAX_API_KEY:sk-xxxx} + model: MiniMax-M3 + moonshot: # 月之暗面(KIMI) + enable: false + api-key: ${MOONSHOT_API_KEY:sk-xxxx} + model: kimi-k2.6 + stepfun: # 阶跃星辰 + enable: false + api-key: ${STEPFUN_API_KEY:sk-xxxx} + model: step-3.7-flash + grok: # Grok + enable: false + api-key: ${GROK_API_KEY:sk-xxxx} + model: grok-4.3 midjourney: enable: true # base-url: https://api.holdai.top/mj-relax/mj base-url: https://api.holdai.top/mj - api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf + api-key: ${MIDJOURNEY_API_KEY:sk-xxxx} notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify suno: enable: true @@ -253,7 +268,7 @@ yudao: base-url: http://127.0.0.1:3001 web-search: enable: true - api-key: sk-40500e52840f4d24b956d0b1d80d9abe + api-key: ${WEB_SEARCH_API_KEY:sk-xxxx} --- #################### 芋道相关配置 #################### @@ -351,4 +366,4 @@ yudao: message-bus: type: redis # 消息总线的类型 -debug: false \ No newline at end of file +debug: false -- libgit2 0.21.4