feat: register qianfan model provider

This commit is contained in:
jimmyzhuu
2026-04-29 15:52:32 +08:00
parent 02bfe30848
commit 9eeca70292
7 changed files with 146 additions and 36 deletions

View File

@@ -14,6 +14,7 @@ from bridge.reply import Reply, ReplyType
from common import const from common import const
from common.log import logger from common.log import logger
from common.utils import expand_path from common.utils import expand_path
from config import conf
from models.openai_compatible_bot import OpenAICompatibleBot from models.openai_compatible_bot import OpenAICompatibleBot
@@ -68,6 +69,7 @@ class AgentLLMModel(LLMModel):
_MODEL_BOT_TYPE_MAP = { _MODEL_BOT_TYPE_MAP = {
"wenxin": const.BAIDU, "wenxin-4": const.BAIDU, "wenxin": const.BAIDU, "wenxin-4": const.BAIDU,
"xunfei": const.XUNFEI, const.QWEN: const.QWEN_DASHSCOPE, "xunfei": const.XUNFEI, const.QWEN: const.QWEN_DASHSCOPE,
const.QIANFAN: const.QIANFAN,
const.MODELSCOPE: const.MODELSCOPE, const.MODELSCOPE: const.MODELSCOPE,
} }
_MODEL_PREFIX_MAP = [ _MODEL_PREFIX_MAP = [
@@ -75,10 +77,10 @@ class AgentLLMModel(LLMModel):
("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), ("claude", const.CLAUDEAPI), ("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), ("claude", const.CLAUDEAPI),
("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT), ("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT),
("doubao", const.DOUBAO), ("deepseek", const.DEEPSEEK), ("doubao", const.DOUBAO), ("deepseek", const.DEEPSEEK),
("ernie", const.QIANFAN),
] ]
def __init__(self, bridge: Bridge, bot_type: str = "chat"): def __init__(self, bridge: Bridge, bot_type: str = "chat"):
from config import conf
super().__init__(model=conf().get("model", const.GPT_41)) super().__init__(model=conf().get("model", const.GPT_41))
self.bridge = bridge self.bridge = bridge
self.bot_type = bot_type self.bot_type = bot_type
@@ -87,7 +89,6 @@ class AgentLLMModel(LLMModel):
@property @property
def model(self): def model(self):
from config import conf
return conf().get("model", const.GPT_41) return conf().get("model", const.GPT_41)
@model.setter @model.setter
@@ -96,8 +97,6 @@ class AgentLLMModel(LLMModel):
def _resolve_bot_type(self, model_name: str) -> str: def _resolve_bot_type(self, model_name: str) -> str:
"""Resolve bot type from model name, matching Bridge.__init__ logic.""" """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"): if conf().get("use_linkai", False) and conf().get("linkai_api_key"):
return const.LINKAI return const.LINKAI
# Support custom bot type configuration # Support custom bot type configuration
@@ -117,8 +116,9 @@ class AgentLLMModel(LLMModel):
return const.MOONSHOT return const.MOONSHOT
if conf().get("bot_type") == "modelscope": if conf().get("bot_type") == "modelscope":
return const.MODELSCOPE return const.MODELSCOPE
lowered_model = model_name.lower()
for prefix, btype in self._MODEL_PREFIX_MAP: for prefix, btype in self._MODEL_PREFIX_MAP:
if model_name.startswith(prefix): if lowered_model.startswith(prefix):
return btype return btype
return const.OPENAI return const.OPENAI
@@ -746,4 +746,4 @@ class AgentBridge:
agent.tools = [t for t in agent.tools if t.name != "web_search"] 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)") logger.info("[AgentBridge] web_search tool removed (API key no longer available)")
except Exception as e: except Exception as e:
logger.debug(f"[AgentBridge] Failed to refresh conditional tools: {e}") logger.debug(f"[AgentBridge] Failed to refresh conditional tools: {e}")

View File

@@ -61,6 +61,11 @@ class Bridge(object):
if model_type and model_type.startswith("deepseek"): if model_type and model_type.startswith("deepseek"):
self.btype["chat"] = const.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]: if model_type in [const.MODELSCOPE]:
self.btype["chat"] = const.MODELSCOPE self.btype["chat"] = const.MODELSCOPE

View File

@@ -3,6 +3,7 @@ OPEN_AI = "openAI"
OPENAI = "openai" OPENAI = "openai"
CHATGPT = "chatGPT" # legacy alias for OPENAI, kept for backward compatibility CHATGPT = "chatGPT" # legacy alias for OPENAI, kept for backward compatibility
BAIDU = "baidu" BAIDU = "baidu"
QIANFAN = "qianfan"
XUNFEI = "xunfei" XUNFEI = "xunfei"
CHATGPTONAZURE = "chatGPTOnAzure" CHATGPTONAZURE = "chatGPTOnAzure"
LINKAI = "linkai" LINKAI = "linkai"
@@ -85,6 +86,12 @@ DEEPSEEK_REASONER = "deepseek-reasoner" # DeepSeek-R1模型
DEEPSEEK_V4_FLASH = "deepseek-v4-flash" # DeepSeek V4 Flash - 默认推荐 (思考模式 + 工具调用) DEEPSEEK_V4_FLASH = "deepseek-v4-flash" # DeepSeek V4 Flash - 默认推荐 (思考模式 + 工具调用)
DEEPSEEK_V4_PRO = "deepseek-v4-pro" # DeepSeek V4 Pro - 复杂任务更强 (思考模式 + 工具调用) 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 (通义千问 - 阿里云 DashScope)
QWEN_TURBO = "qwen-turbo" QWEN_TURBO = "qwen-turbo"
QWEN_PLUS = "qwen-plus" QWEN_PLUS = "qwen-plus"
@@ -159,6 +166,9 @@ MODEL_LIST = [
# DeepSeek # DeepSeek
DEEPSEEK_V4_FLASH, DEEPSEEK_V4_PRO, DEEPSEEK_CHAT, DEEPSEEK_REASONER, 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, MINIMAX_M2_7, MINIMAX_M2_7_HIGHSPEED, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5, MiniMax, MINIMAX_M2_7, MINIMAX_M2_7_HIGHSPEED, MINIMAX_M2_5, MINIMAX_M2_1, MINIMAX_M2_1_LIGHTNING, MINIMAX_M2, MINIMAX_ABAB6_5,

View File

@@ -3,6 +3,8 @@
"model": "deepseek-v4-flash", "model": "deepseek-v4-flash",
"deepseek_api_key": "", "deepseek_api_key": "",
"deepseek_api_base": "https://api.deepseek.com/v1", "deepseek_api_base": "https://api.deepseek.com/v1",
"qianfan_api_key": "",
"qianfan_api_base": "https://qianfan.baidubce.com/v2",
"minimax_api_key": "", "minimax_api_key": "",
"zhipu_ai_api_key": "", "zhipu_ai_api_key": "",
"ark_api_key": "", "ark_api_key": "",

View File

@@ -76,6 +76,9 @@ available_setting = {
"baidu_wenxin_api_key": "", # Baidu api key "baidu_wenxin_api_key": "", # Baidu api key
"baidu_wenxin_secret_key": "", # Baidu secret key "baidu_wenxin_secret_key": "", # Baidu secret key
"baidu_wenxin_prompt_enabled": False, # Enable prompt if you are using ernie character model "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 # 讯飞星火API
"xunfei_app_id": "", # 讯飞应用ID "xunfei_app_id": "", # 讯飞应用ID
"xunfei_api_key": "", # 讯飞 API key "xunfei_api_key": "", # 讯飞 API key
@@ -386,6 +389,8 @@ def load_config():
"minimax_api_base": "MINIMAX_API_BASE", "minimax_api_base": "MINIMAX_API_BASE",
"deepseek_api_key": "DEEPSEEK_API_KEY", "deepseek_api_key": "DEEPSEEK_API_KEY",
"deepseek_api_base": "DEEPSEEK_API_BASE", "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_key": "ZHIPU_AI_API_KEY",
"zhipu_ai_api_base": "ZHIPU_AI_API_BASE", "zhipu_ai_api_base": "ZHIPU_AI_API_BASE",
"moonshot_api_key": "MOONSHOT_API_KEY", "moonshot_api_key": "MOONSHOT_API_KEY",

View File

@@ -42,6 +42,49 @@ CLI_ONLY_COMMANDS = {"start", "stop", "restart"}
CHAT_ONLY_COMMANDS = set() # context is allowed in both, but behaves differently 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( @plugins.register(
name="cow_cli", name="cow_cli",
desc="Handle cow/slash commands in chat messages", desc="Handle cow/slash commands in chat messages",
@@ -428,36 +471,7 @@ class CowCliPlugin(Plugin):
@staticmethod @staticmethod
def _resolve_bot_type_for_model(model_name: str) -> str: def _resolve_bot_type_for_model(model_name: str) -> str:
"""Resolve bot_type from model name, reusing AgentBridge mapping.""" return CowCli._resolve_bot_type_for_model(model_name)
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
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# install-browser (shared logic with cow install-browser CLI) # install-browser (shared logic with cow install-browser CLI)
@@ -1172,3 +1186,7 @@ class CowCliPlugin(Plugin):
def get_help_text(self, **kwargs): def get_help_text(self, **kwargs):
return "在对话中使用 /help 或 cow help 查看可用命令" return "在对话中使用 /help 或 cow help 查看可用命令"
if _COW_CLI_SET_PLUGIN_PATH:
plugins.instance.current_plugin_path = None

View File

@@ -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()