From 9eeca70292ad3866449f4b890472715362b12f49 Mon Sep 17 00:00:00 2001 From: jimmyzhuu Date: Wed, 29 Apr 2026 15:52:32 +0800 Subject: [PATCH] feat: register qianfan model provider --- bridge/agent_bridge.py | 12 +++--- bridge/bridge.py | 5 +++ common/const.py | 10 +++++ config-template.json | 2 + config.py | 5 +++ plugins/cow_cli/cow_cli.py | 78 +++++++++++++++++++++------------- tests/test_qianfan_provider.py | 70 ++++++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 tests/test_qianfan_provider.py diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 3701df77..330a5d16 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -14,6 +14,7 @@ from bridge.reply import Reply, ReplyType from common import const from common.log import logger from common.utils import expand_path +from config import conf from models.openai_compatible_bot import OpenAICompatibleBot @@ -68,6 +69,7 @@ class AgentLLMModel(LLMModel): _MODEL_BOT_TYPE_MAP = { "wenxin": const.BAIDU, "wenxin-4": const.BAIDU, "xunfei": const.XUNFEI, const.QWEN: const.QWEN_DASHSCOPE, + const.QIANFAN: const.QIANFAN, const.MODELSCOPE: const.MODELSCOPE, } _MODEL_PREFIX_MAP = [ @@ -75,10 +77,10 @@ class AgentLLMModel(LLMModel): ("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), ("claude", const.CLAUDEAPI), ("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT), ("doubao", const.DOUBAO), ("deepseek", const.DEEPSEEK), + ("ernie", const.QIANFAN), ] def __init__(self, bridge: Bridge, bot_type: str = "chat"): - from config import conf super().__init__(model=conf().get("model", const.GPT_41)) self.bridge = bridge self.bot_type = bot_type @@ -87,7 +89,6 @@ class AgentLLMModel(LLMModel): @property def model(self): - from config import conf return conf().get("model", const.GPT_41) @model.setter @@ -96,8 +97,6 @@ class AgentLLMModel(LLMModel): def _resolve_bot_type(self, model_name: str) -> str: """Resolve bot type from model name, matching Bridge.__init__ logic.""" - from config import conf - if conf().get("use_linkai", False) and conf().get("linkai_api_key"): return const.LINKAI # Support custom bot type configuration @@ -117,8 +116,9 @@ class AgentLLMModel(LLMModel): return const.MOONSHOT if conf().get("bot_type") == "modelscope": return const.MODELSCOPE + lowered_model = model_name.lower() for prefix, btype in self._MODEL_PREFIX_MAP: - if model_name.startswith(prefix): + if lowered_model.startswith(prefix): return btype return const.OPENAI @@ -746,4 +746,4 @@ class AgentBridge: agent.tools = [t for t in agent.tools if t.name != "web_search"] logger.info("[AgentBridge] web_search tool removed (API key no longer available)") except Exception as e: - logger.debug(f"[AgentBridge] Failed to refresh conditional tools: {e}") \ No newline at end of file + logger.debug(f"[AgentBridge] Failed to refresh conditional tools: {e}") diff --git a/bridge/bridge.py b/bridge/bridge.py index 3cf55a80..753e394a 100644 --- a/bridge/bridge.py +++ b/bridge/bridge.py @@ -61,6 +61,11 @@ class Bridge(object): if model_type and model_type.startswith("deepseek"): self.btype["chat"] = const.DEEPSEEK + if model_type and isinstance(model_type, str): + lowered_model_type = model_type.lower() + if lowered_model_type == const.QIANFAN or lowered_model_type.startswith("ernie"): + self.btype["chat"] = const.QIANFAN + if model_type in [const.MODELSCOPE]: self.btype["chat"] = const.MODELSCOPE diff --git a/common/const.py b/common/const.py index b10a8eaa..ea8d80e5 100644 --- a/common/const.py +++ b/common/const.py @@ -3,6 +3,7 @@ OPEN_AI = "openAI" OPENAI = "openai" CHATGPT = "chatGPT" # legacy alias for OPENAI, kept for backward compatibility BAIDU = "baidu" +QIANFAN = "qianfan" XUNFEI = "xunfei" CHATGPTONAZURE = "chatGPTOnAzure" LINKAI = "linkai" @@ -85,6 +86,12 @@ DEEPSEEK_REASONER = "deepseek-reasoner" # DeepSeek-R1模型 DEEPSEEK_V4_FLASH = "deepseek-v4-flash" # DeepSeek V4 Flash - 默认推荐 (思考模式 + 工具调用) DEEPSEEK_V4_PRO = "deepseek-v4-pro" # DeepSeek V4 Pro - 复杂任务更强 (思考模式 + 工具调用) +# Baidu Qianfan / ERNIE +ERNIE_45_TURBO_128K = "ernie-4.5-turbo-128k" +ERNIE_45_TURBO_32K = "ernie-4.5-turbo-32k" +ERNIE_X1_TURBO_32K = "ernie-x1-turbo-32k" +ERNIE_4_TURBO_8K = "ERNIE-4.0-Turbo-8K" + # Qwen (通义千问 - 阿里云 DashScope) QWEN_TURBO = "qwen-turbo" QWEN_PLUS = "qwen-plus" @@ -159,6 +166,9 @@ MODEL_LIST = [ # DeepSeek DEEPSEEK_V4_FLASH, DEEPSEEK_V4_PRO, DEEPSEEK_CHAT, DEEPSEEK_REASONER, + # Baidu Qianfan / ERNIE + QIANFAN, ERNIE_45_TURBO_128K, ERNIE_45_TURBO_32K, ERNIE_X1_TURBO_32K, ERNIE_4_TURBO_8K, + # MiniMax MiniMax, MINIMAX_M2_7, MINIMAX_M2_7_HIGHSPEED, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5, diff --git a/config-template.json b/config-template.json index 22942793..3a6c38b1 100644 --- a/config-template.json +++ b/config-template.json @@ -3,6 +3,8 @@ "model": "deepseek-v4-flash", "deepseek_api_key": "", "deepseek_api_base": "https://api.deepseek.com/v1", + "qianfan_api_key": "", + "qianfan_api_base": "https://qianfan.baidubce.com/v2", "minimax_api_key": "", "zhipu_ai_api_key": "", "ark_api_key": "", diff --git a/config.py b/config.py index a95b542d..5063bcab 100644 --- a/config.py +++ b/config.py @@ -76,6 +76,9 @@ available_setting = { "baidu_wenxin_api_key": "", # Baidu api key "baidu_wenxin_secret_key": "", # Baidu secret key "baidu_wenxin_prompt_enabled": False, # Enable prompt if you are using ernie character model + # Baidu Qianfan / ERNIE OpenAI-compatible API + "qianfan_api_key": "", # Baidu Qianfan API key in bce-v3 format + "qianfan_api_base": "https://qianfan.baidubce.com/v2", # Qianfan OpenAI-compatible API base # 讯飞星火API "xunfei_app_id": "", # 讯飞应用ID "xunfei_api_key": "", # 讯飞 API key @@ -386,6 +389,8 @@ def load_config(): "minimax_api_base": "MINIMAX_API_BASE", "deepseek_api_key": "DEEPSEEK_API_KEY", "deepseek_api_base": "DEEPSEEK_API_BASE", + "qianfan_api_key": "QIANFAN_API_KEY", + "qianfan_api_base": "QIANFAN_API_BASE", "zhipu_ai_api_key": "ZHIPU_AI_API_KEY", "zhipu_ai_api_base": "ZHIPU_AI_API_BASE", "moonshot_api_key": "MOONSHOT_API_KEY", diff --git a/plugins/cow_cli/cow_cli.py b/plugins/cow_cli/cow_cli.py index 6a449314..d95abf62 100644 --- a/plugins/cow_cli/cow_cli.py +++ b/plugins/cow_cli/cow_cli.py @@ -42,6 +42,49 @@ CLI_ONLY_COMMANDS = {"start", "stop", "restart"} CHAT_ONLY_COMMANDS = set() # context is allowed in both, but behaves differently +_COW_CLI_SET_PLUGIN_PATH = plugins.instance.current_plugin_path is None +if _COW_CLI_SET_PLUGIN_PATH: + plugins.instance.current_plugin_path = os.path.dirname(__file__) + + +class CowCli: + @staticmethod + def _resolve_bot_type_for_model(model_name: str) -> str: + """Resolve bot_type from model name, reusing AgentBridge mapping.""" + from common import const + _EXACT = { + "wenxin": const.BAIDU, "wenxin-4": const.BAIDU, + "xunfei": const.XUNFEI, const.QWEN: const.QWEN_DASHSCOPE, + const.QIANFAN: const.QIANFAN, + const.MODELSCOPE: const.MODELSCOPE, + const.MOONSHOT: const.MOONSHOT, + "moonshot-v1-8k": const.MOONSHOT, "moonshot-v1-32k": const.MOONSHOT, + "moonshot-v1-128k": const.MOONSHOT, + } + _PREFIX = [ + ("qwen", const.QWEN_DASHSCOPE), ("qwq", const.QWEN_DASHSCOPE), + ("qvq", const.QWEN_DASHSCOPE), + ("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), + ("claude", const.CLAUDEAPI), + ("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT), + ("doubao", const.DOUBAO), ("deepseek", const.DEEPSEEK), + ("ernie", const.QIANFAN), + ] + if not model_name: + return const.OPENAI + if model_name in _EXACT: + return _EXACT[model_name] + if model_name.lower().startswith("minimax") or model_name in ["abab6.5-chat"]: + return const.MiniMax + if model_name in [const.QWEN_TURBO, const.QWEN_PLUS, const.QWEN_MAX]: + return const.QWEN_DASHSCOPE + lowered_model = model_name.lower() + for prefix, btype in _PREFIX: + if lowered_model.startswith(prefix): + return btype + return const.OPENAI + + @plugins.register( name="cow_cli", desc="Handle cow/slash commands in chat messages", @@ -428,36 +471,7 @@ class CowCliPlugin(Plugin): @staticmethod def _resolve_bot_type_for_model(model_name: str) -> str: - """Resolve bot_type from model name, reusing AgentBridge mapping.""" - from common import const - _EXACT = { - "wenxin": const.BAIDU, "wenxin-4": const.BAIDU, - "xunfei": const.XUNFEI, const.QWEN: const.QWEN_DASHSCOPE, - const.MODELSCOPE: const.MODELSCOPE, - const.MOONSHOT: const.MOONSHOT, - "moonshot-v1-8k": const.MOONSHOT, "moonshot-v1-32k": const.MOONSHOT, - "moonshot-v1-128k": const.MOONSHOT, - } - _PREFIX = [ - ("qwen", const.QWEN_DASHSCOPE), ("qwq", const.QWEN_DASHSCOPE), - ("qvq", const.QWEN_DASHSCOPE), - ("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), - ("claude", const.CLAUDEAPI), - ("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT), - ("doubao", const.DOUBAO), ("deepseek", const.DEEPSEEK), - ] - if not model_name: - return const.OPENAI - if model_name in _EXACT: - return _EXACT[model_name] - if model_name.lower().startswith("minimax") or model_name in ["abab6.5-chat"]: - return const.MiniMax - if model_name in [const.QWEN_TURBO, const.QWEN_PLUS, const.QWEN_MAX]: - return const.QWEN_DASHSCOPE - for prefix, btype in _PREFIX: - if model_name.startswith(prefix): - return btype - return const.OPENAI + return CowCli._resolve_bot_type_for_model(model_name) # ------------------------------------------------------------------ # install-browser (shared logic with cow install-browser CLI) @@ -1172,3 +1186,7 @@ class CowCliPlugin(Plugin): def get_help_text(self, **kwargs): return "在对话中使用 /help 或 cow help 查看可用命令" + + +if _COW_CLI_SET_PLUGIN_PATH: + plugins.instance.current_plugin_path = None diff --git a/tests/test_qianfan_provider.py b/tests/test_qianfan_provider.py new file mode 100644 index 00000000..7a07fccb --- /dev/null +++ b/tests/test_qianfan_provider.py @@ -0,0 +1,70 @@ +# encoding:utf-8 +import os +import sys +import unittest +from unittest.mock import MagicMock, patch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + + +class TestQianfanConstantsAndRouting(unittest.TestCase): + def test_qianfan_provider_constant_defined(self): + from common import const + + self.assertEqual(const.QIANFAN, "qianfan") + + def test_ernie_constants_are_in_model_list(self): + from common import const + + self.assertEqual(const.ERNIE_45_TURBO_128K, "ernie-4.5-turbo-128k") + self.assertEqual(const.ERNIE_45_TURBO_32K, "ernie-4.5-turbo-32k") + self.assertEqual(const.ERNIE_X1_TURBO_32K, "ernie-x1-turbo-32k") + self.assertIn(const.QIANFAN, const.MODEL_LIST) + self.assertIn(const.ERNIE_45_TURBO_128K, const.MODEL_LIST) + self.assertIn(const.ERNIE_45_TURBO_32K, const.MODEL_LIST) + self.assertIn(const.ERNIE_X1_TURBO_32K, const.MODEL_LIST) + + def test_qianfan_config_keys_are_available(self): + import config + + self.assertIn("qianfan_api_key", config.available_setting) + self.assertIn("qianfan_api_base", config.available_setting) + + def test_agent_bridge_routes_ernie_models_to_qianfan(self): + from bridge.agent_bridge import AgentLLMModel + from common import const + + model = AgentLLMModel.__new__(AgentLLMModel) + fake_conf = MagicMock() + fake_conf.get.side_effect = lambda key, default=None: { + "use_linkai": False, + "linkai_api_key": "", + "bot_type": "", + }.get(key, default) + + with patch("bridge.agent_bridge.conf", return_value=fake_conf): + self.assertEqual( + AgentLLMModel._resolve_bot_type(model, "ernie-4.5-turbo-128k"), + const.QIANFAN, + ) + self.assertEqual( + AgentLLMModel._resolve_bot_type(model, "qianfan"), + const.QIANFAN, + ) + + def test_cow_cli_routes_ernie_models_to_qianfan(self): + from common import const + from plugins.cow_cli.cow_cli import CowCli + + self.assertEqual( + CowCli._resolve_bot_type_for_model("ernie-4.5-turbo-128k"), + const.QIANFAN, + ) + self.assertEqual( + CowCli._resolve_bot_type_for_model("qianfan"), + const.QIANFAN, + ) + + +if __name__ == "__main__": + unittest.main()