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 aa0ef5d6e2..47a4d2d719 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 @@ -34,6 +34,7 @@ public enum AiPlatformEnum implements ArrayValuable { OPENAI("OpenAI", "OpenAI"), // OpenAI 官方 AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软 ANTHROPIC("Anthropic", "Anthropic"), // Anthropic Claude + GEMINI("Gemini", "Gemini"), // 谷歌 Gemini OLLAMA("Ollama", "Ollama"), STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI 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 4ff7c9e4dc..0d5360b770 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 @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; 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.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.siliconflow.SiliconFlowApiConstants; @@ -51,6 +52,34 @@ public class AiAutoConfiguration { // ========== 各种 AI Client 创建 ========== + @Bean + @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true") + public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.GeminiProperties properties = yudaoAiProperties.getGemini(); + return buildGeminiChatClient(properties); + } + + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.GeminiProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(GeminiChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(GeminiChatModel.BASE_URL) + .completionsPath(GeminiChatModel.COMPLETE_PATH) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); + return new GeminiChatModel(openAiChatModel); + } + @Bean @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { @@ -150,17 +179,22 @@ 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(OpenAiApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL) - .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()) - .build()) + .openAiApi(builder.build()) .defaultOptions(OpenAiChatOptions.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); 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 7c26aa89ca..9c028c6cf0 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 @@ -13,6 +13,11 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @Data public class YudaoAiProperties { + /** + * 谷歌 Gemini + */ + private GeminiProperties gemini; + /** * 字节豆包 */ @@ -54,6 +59,19 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") private SunoProperties suno; + @Data + public static class GeminiProperties { + + private String enable; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + @Data public static class DouBaoProperties { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index d6a26ee0e3..924f0f78ab 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; 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.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.siliconflow.SiliconFlowApiConstants; @@ -173,6 +174,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildAzureOpenAiChatModel(apiKey, url); case ANTHROPIC: return buildAnthropicChatModel(apiKey, url); + case GEMINI: + return buildGeminiChatModel(apiKey); case OLLAMA: return buildOllamaChatModel(url); default: @@ -213,6 +216,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(AzureOpenAiChatModel.class); case ANTHROPIC: return SpringUtil.getBean(AnthropicChatModel.class); + case GEMINI: + return SpringUtil.getBean(GeminiChatModel.class); case OLLAMA: return SpringUtil.getBean(OllamaChatModel.class); default: @@ -534,6 +539,15 @@ public class AiModelFactoryImpl implements AiModelFactory { .build(); } + /** + * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.GeminiProperties)} + */ + private static GeminiChatModel buildGeminiChatModel(String apiKey) { + YudaoAiProperties.GeminiProperties properties = SpringUtil.getBean(YudaoAiProperties.class) + .getGemini().setApiKey(apiKey); + return new AiAutoConfiguration().buildGeminiChatClient(properties); + } + /** * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 */ diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/gemini/GeminiChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/gemini/GeminiChatModel.java new file mode 100644 index 0000000000..378a0af1fb --- /dev/null +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/gemini/GeminiChatModel.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini; + +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.openai.OpenAiChatModel; +import reactor.core.publisher.Flux; + +/** + * 谷歌 Gemini {@link ChatModel} 实现类,基于 Google AI Studio 提供的 OpenAI 兼容方案 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class GeminiChatModel implements ChatModel { + + public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"; + public static final String COMPLETE_PATH = "/chat/completions"; + + public static final String MODEL_DEFAULT = "gemini-2.5-flash"; + + /** + * 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return openAiChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.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 d97e263987..8c18c1e8b0 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 @@ -18,28 +18,34 @@ import reactor.core.publisher.Flux; @RequiredArgsConstructor public class XingHuoChatModel implements ChatModel { - public static final String BASE_URL = "https://spark-api-open.xf-yun.com"; + public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com"; - public static final String MODEL_DEFAULT = "generalv3.5"; + 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"; /** - * 兼容 OpenAI 接口,进行复用 + * 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite */ - private final OpenAiChatModel openAiChatModel; + public static final String MODEL_DEFAULT = "4.0Ultra"; + + /** + * v1 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModelV1; @Override public ChatResponse call(Prompt prompt) { - return openAiChatModel.call(prompt); + return openAiChatModelV1.call(prompt); } @Override public Flux stream(Prompt prompt) { - return openAiChatModel.stream(prompt); + return openAiChatModelV1.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return openAiChatModel.getDefaultOptions(); + return openAiChatModelV1.getDefaultOptions(); } } 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 624d07340b..10dcf73e3e 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 @@ -12,6 +12,7 @@ import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatOptions; @@ -47,6 +48,9 @@ public class AiUtils { .withToolNames(toolNames).withToolContext(toolContext).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + case DEEP_SEEK: + return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .toolNames(toolNames).toolContext(toolContext).build(); case ZHI_PU: return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolNames(toolNames).toolContext(toolContext).build(); @@ -57,7 +61,7 @@ public class AiUtils { return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolNames(toolNames).toolContext(toolContext).build(); case OPENAI: - case DEEP_SEEK: // 复用 OpenAI 客户端 + case GEMINI: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 case HUN_YUAN: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端 diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java new file mode 100644 index 0000000000..964a5f3c36 --- /dev/null +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; + +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; +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; + +/** + * {@link GeminiChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class GeminiChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(GeminiChatModel.BASE_URL) + .completionsPath(GeminiChatModel.COMPLETE_PATH) + .apiKey("AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ") + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(GeminiChatModel.MODEL_DEFAULT) // 模型 + .temperature(0.7) + .build()) + .build(); + + private final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + 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); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).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 5d8dae2010..72c8eca29c 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 @@ -25,11 +25,13 @@ public class XingHuoChatModelTests { private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL) + .baseUrl(XingHuoChatModel.BASE_URL_V2) + .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2) .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey .build()) .defaultOptions(OpenAiChatOptions.builder() - .model("generalv3.5") // 模型 +// .model("generalv3.5") // 模型 + .model("x1") // 模型 .temperature(0.7) .build()) .build(); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index b9cfd2ae67..7eddd1a60a 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -197,6 +197,10 @@ spring: yudao: ai: + gemini: # 谷歌 Gemini + enable: true + api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ + model: gemini-2.5-flash doubao: # 字节豆包 enable: true api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272