From ae0e637b589ae563d2141c15be15264093835d9e Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Fri, 21 Aug 2026 15:34:58 +0000 Subject: [PATCH] feat: add OrcaRouter as an optional AI engine provider Adds a named OrcaRouter provider alongside the existing DeepSeek integration. When ORCAROUTER_API_KEY is set in .env, the system routes all analysis modules (technical/fundamental/fund-flow agents, smart monitor, news flow, sector strategy, macro cycle) through OrcaRouter's OpenAI-compatible gateway at https://api.orcarouter.ai/v1, defaulting to the orcarouter/auto model. - llm_client.py: factory selecting OrcaRouterClient when the key is set - orcarouter_client.py: OrcaRouter client (subclasses DeepSeekClient, reuses all prompt/chat logic) - config.py / config_manager.py / .env.example / env_example.txt / update_env_example.py: ORCAROUTER_API_KEY/BASE_URL/MODEL settings - app.py: OrcaRouter section in the environment-config UI - model_config.py: orcarouter/auto preset entry - smart_monitor_deepseek.py: picks OrcaRouter endpoint when configured - test_orcarouter_client.py: unit tests for provider selection DeepSeek remains the default when OrcaRouter is not configured; no existing behavior is changed. Disclosure: I'm an engineer on the OrcaRouter team. Co-Authored-By: Claude Signed-off-by: XiaoHuo888-hue --- .env.example | 8 +++ README.md | 26 ++++++++++ ai_agents.py | 6 +-- app.py | 56 +++++++++++++++++++++ config.py | 15 ++++-- config_manager.py | 31 +++++++++++- env_example.txt | 6 +++ llm_client.py | 21 ++++++++ longhubang_agents.py | 6 +-- macro_analysis_agents.py | 4 +- macro_cycle_agents.py | 4 +- main_force_analysis.py | 1 - model_config.py | 1 + news_flow_agents.py | 10 ++-- orcarouter_client.py | 23 +++++++++ sector_strategy_agents.py | 6 +-- sector_strategy_engine.py | 6 +-- smart_monitor_deepseek.py | 22 ++++++--- test_orcarouter_client.py | 101 ++++++++++++++++++++++++++++++++++++++ update_env_example.py | 8 +++ 20 files changed, 327 insertions(+), 34 deletions(-) create mode 100644 llm_client.py create mode 100644 orcarouter_client.py create mode 100644 test_orcarouter_client.py diff --git a/.env.example b/.env.example index 9035196d..905ecd92 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,14 @@ DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 DEFAULT_MODEL_NAME=deepseek-chat +# ========== OrcaRouter API配置(可选)========== +# 设置了 ORCAROUTER_API_KEY 后,系统将优先使用 OrcaRouter 作为 AI 引擎 +# OrcaRouter:https://www.orcarouter.ai | API:https://api.orcarouter.ai/v1 +ORCAROUTER_API_KEY= +ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1 +ORCAROUTER_MODEL=orcarouter/auto + + # ========== You.com Research API配置(可选)========== # You.com API密钥(用于深度研究功能,需要配合 Research API 使用) # 获取地址:https://you.com/platform/api-keys diff --git a/README.md b/README.md index 7ccd828b..f88a9eda 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,13 @@ playwright install chromium # ===== 必填 ===== DEEPSEEK_API_KEY=your_key_here # DeepSeek API密钥(核心AI引擎) +# ===== AI 引擎(可选,二选一)===== +# 设置了 ORCAROUTER_API_KEY 后,系统优先使用 OrcaRouter 作为 AI 引擎(无需再配 DeepSeek) +# OrcaRouter:https://www.orcarouter.ai | API:https://api.orcarouter.ai/v1 +ORCAROUTER_API_KEY=your_orcarouter_key_here +ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1 +ORCAROUTER_MODEL=orcarouter/auto + # ===== 数据源(可选)===== TUSHARE_TOKEN=your_token_here # Tushare Token(https://tushare.pro) YDC_API_KEY=your_ydc_api_key_here # You.com API密钥(智策板块Research功能用) @@ -74,6 +81,24 @@ MINIQMT_HOST=127.0.0.1 MINIQMT_PORT=58080 ``` +### 使用 OrcaRouter 作为 AI 引擎(可选) + +[OrcaRouter](https://www.orcarouter.ai) 是 OpenAI 兼容的统一模型网关,一个 key 即可路由到 DeepSeek、Qwen、Kimi 等主流模型。在 `.env` 中设置 `ORCAROUTER_API_KEY` 后,系统将**优先使用 OrcaRouter** 作为所有分析模块的 AI 引擎(无需配置 DeepSeek): + +```env +# ===== OrcaRouter 配置 ===== +ORCAROUTER_API_KEY=your_orcarouter_key_here +ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1 # 一般无需修改 +ORCAROUTER_MODEL=orcarouter/auto # 自动路由最优模型 +``` + +支持的模型(也可在「环境配置」界面查看与修改): +- `orcarouter/auto` — 自动路由(默认) +- `deepseek/deepseek-v4-pro` — DeepSeek V4 Pro +- `qwen/qwen3.6-flash` — Qwen 3.6 Flash + +未设置 `ORCAROUTER_API_KEY` 时,系统照常使用上方 DeepSeek 配置,两者互不影响。 + ### 测试 You.com Research 模块的测试位于 `utils/test_youchannels_research.py`。 @@ -208,6 +233,7 @@ python3 -m pytest utils/test_youchannels_research.py::TestGetYoudotcomResearchIn - ✅ **移除所有模型选择下拉框** — 龙虎榜、主力选股、智策板块等页面不再需要手动选模型 - ✅ **环境配置 UI 新增模型输入** — 在「环境配置」中可直接输入模型名称,附常用模型参考 - ✅ **支持任意 OpenAI 兼容模型** — DeepSeek、通义千问、GPT-4o 等一键切换 +- ✅ **支持 [OrcaRouter](https://www.orcarouter.ai) 网关** — 设置 `ORCAROUTER_API_KEY` 后一键切换到 OrcaRouter 统一网关,路由 DeepSeek/Qwen/Kimi 等模型 **切换模型只需一步:** ```env diff --git a/ai_agents.py b/ai_agents.py index f23080a9..5960f747 100644 --- a/ai_agents.py +++ b/ai_agents.py @@ -1,14 +1,14 @@ -from deepseek_client import DeepSeekClient +from llm_client import get_llm_client from typing import Dict, Any import time import config class StockAnalysisAgents: """股票分析AI智能体集合""" - + def __init__(self, model=None): self.model = model or config.DEFAULT_MODEL_NAME - self.deepseek_client = DeepSeekClient(model=self.model) + self.deepseek_client = get_llm_client(model=model) def technical_analyst_agent(self, stock_info: Dict, stock_data: Any, indicators: Dict) -> Dict[str, Any]: """技术面分析智能体""" diff --git a/app.py b/app.py index ece8e5b5..2b39ff2f 100644 --- a/app.py +++ b/app.py @@ -2234,6 +2234,62 @@ def display_config_manager(): st.info("💡 如何获取DeepSeek API密钥?\n\n1. 访问 https://platform.deepseek.com\n2. 注册/登录账号\n3. 进入API密钥管理页面\n4. 创建新的API密钥\n5. 复制密钥并粘贴到上方输入框") + st.markdown("---") + st.markdown("### OrcaRouter API配置(可选)") + st.markdown("[OrcaRouter](https://www.orcarouter.ai) 是 OpenAI 兼容的统一模型网关,设置 `ORCAROUTER_API_KEY` 后将优先使用 OrcaRouter 作为 AI 引擎(未设置时仍使用上方 DeepSeek)。") + st.markdown("OrcaRouter: https://api.orcarouter.ai/v1") + + orca_api_key_info = config_info["ORCAROUTER_API_KEY"] + current_orca_api_key = st.session_state.temp_config.get("ORCAROUTER_API_KEY", "") + + new_orca_api_key = st.text_input( + f"🔑 {orca_api_key_info['description']}", + value=current_orca_api_key, + type="password", + help="从 https://www.orcarouter.ai 获取API密钥", + key="input_orcarouter_api_key" + ) + st.session_state.temp_config["ORCAROUTER_API_KEY"] = new_orca_api_key + + if new_orca_api_key: + st.success("✅ OrcaRouter 已启用,系统将使用 OrcaRouter 引擎") + + st.markdown("---") + + orca_base_url_info = config_info["ORCAROUTER_BASE_URL"] + current_orca_base_url = st.session_state.temp_config.get("ORCAROUTER_BASE_URL", "https://api.orcarouter.ai/v1") + + new_orca_base_url = st.text_input( + f"🌐 {orca_base_url_info['description']}", + value=current_orca_base_url, + help="一般无需修改,保持默认即可", + key="input_orcarouter_base_url" + ) + st.session_state.temp_config["ORCAROUTER_BASE_URL"] = new_orca_base_url + + st.markdown("---") + + orca_model_info = config_info["ORCAROUTER_MODEL"] + current_orca_model = st.session_state.temp_config.get("ORCAROUTER_MODEL", "orcarouter/auto") + + new_orca_model = st.text_input( + f"🤖 {orca_model_info['description']}", + value=current_orca_model, + help="OrcaRouter 自动路由模型,修改后重启生效", + key="input_orcarouter_model" + ) + st.session_state.temp_config["ORCAROUTER_MODEL"] = new_orca_model + + if new_orca_model: + st.success(f"✅ OrcaRouter 模型: **{new_orca_model}**") + + st.markdown(""" + **OrcaRouter 常用模型:** + - `orcarouter/auto` — 自动路由(默认) + - `deepseek/deepseek-v4-pro` — DeepSeek V4 Pro + - `qwen/qwen3.6-flash` — Qwen 3.6 Flash + """) + with tab2: st.markdown("### Tushare数据接口(可选)") st.markdown("Tushare提供更丰富的A股财务数据,配置后可以获取更详细的财务分析。") diff --git a/config.py b/config.py index 730dfc53..aed6bbbf 100644 --- a/config.py +++ b/config.py @@ -8,11 +8,20 @@ DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") -# You.com API配置 -YDC_API_KEY = os.getenv("YDC_API_KEY", "") +# OrcaRouter API配置(可选,设置了 ORCAROUTER_API_KEY 后作为 AI 引擎使用) +ORCAROUTER_API_KEY = os.getenv("ORCAROUTER_API_KEY", "") +ORCAROUTER_BASE_URL = os.getenv("ORCAROUTER_BASE_URL", "https://api.orcarouter.ai/v1") +ORCAROUTER_MODEL = os.getenv("ORCAROUTER_MODEL", "orcarouter/auto") # 默认AI模型名称(支持任何OpenAI兼容的模型) -DEFAULT_MODEL_NAME = os.getenv("DEFAULT_MODEL_NAME", "deepseek-chat") +# 设置了 ORCAROUTER_API_KEY 时,默认模型自动落到 OrcaRouter 的配置模型 +DEFAULT_MODEL_NAME = os.getenv( + "DEFAULT_MODEL_NAME", + ORCAROUTER_MODEL if ORCAROUTER_API_KEY else "deepseek-chat" +) + +# You.com API配置 +YDC_API_KEY = os.getenv("YDC_API_KEY", "") # 其他配置 TUSHARE_TOKEN = os.getenv("TUSHARE_TOKEN", "") diff --git a/config_manager.py b/config_manager.py index 91b5d3b7..0dd39d05 100644 --- a/config_manager.py +++ b/config_manager.py @@ -32,6 +32,24 @@ def __init__(self, env_file: str = ".env"): "required": False, "type": "text" }, + "ORCAROUTER_API_KEY": { + "value": "", + "description": "OrcaRouter API密钥(可选,设置后优先使用OrcaRouter引擎)", + "required": False, + "type": "password" + }, + "ORCAROUTER_BASE_URL": { + "value": "https://api.orcarouter.ai/v1", + "description": "OrcaRouter API地址", + "required": False, + "type": "text" + }, + "ORCAROUTER_MODEL": { + "value": "orcarouter/auto", + "description": "OrcaRouter模型名称", + "required": False, + "type": "text" + }, "TUSHARE_TOKEN": { "value": "", "description": "Tushare数据接口Token(可选)", @@ -202,6 +220,9 @@ def write_env(self, config: Dict[str, str]) -> bool: lines.append(f'DEEPSEEK_API_KEY="{full_config.get("DEEPSEEK_API_KEY", "")}"') lines.append(f'DEEPSEEK_BASE_URL="{full_config.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")}"') lines.append(f'DEFAULT_MODEL_NAME="{full_config.get("DEFAULT_MODEL_NAME", "deepseek-chat")}"') + lines.append(f'ORCAROUTER_API_KEY="{full_config.get("ORCAROUTER_API_KEY", "")}"') + lines.append(f'ORCAROUTER_BASE_URL="{full_config.get("ORCAROUTER_BASE_URL", "https://api.orcarouter.ai/v1")}"') + lines.append(f'ORCAROUTER_MODEL="{full_config.get("ORCAROUTER_MODEL", "orcarouter/auto")}"') lines.append("") # 数据接口配置 @@ -240,6 +261,7 @@ def write_env(self, config: Dict[str, str]) -> bool: # 保留其他非标准自定义键 written_keys = { "DEEPSEEK_API_KEY", "DEEPSEEK_BASE_URL", "DEFAULT_MODEL_NAME", + "ORCAROUTER_API_KEY", "ORCAROUTER_BASE_URL", "ORCAROUTER_MODEL", "TUSHARE_TOKEN", "TDX_BASE_URL", "YDC_API_KEY", "YDC_RESEARCH_EFFORT", "MINIQMT_ENABLED", "MINIQMT_ACCOUNT_ID", "MINIQMT_HOST", "MINIQMT_PORT", "EMAIL_ENABLED", "SMTP_SERVER", "SMTP_PORT", "EMAIL_FROM", "EMAIL_PASSWORD", "EMAIL_TO", @@ -280,17 +302,22 @@ def get_config_info(self) -> Dict[str, Dict[str, Any]]: def validate_config(self, config: Dict[str, str]) -> tuple[bool, str]: """验证配置""" + # 设置了 OrcaRouter 密钥时,DeepSeek 密钥不再是必填项(二选一) + has_orcarouter = bool(config.get("ORCAROUTER_API_KEY")) + # 检查必填项 for key, info in self.default_config.items(): + if key == "DEEPSEEK_API_KEY" and has_orcarouter: + continue if info["required"] and not config.get(key): return False, f"必填项 {info['description']} 不能为空" - + # 验证API Key格式(简单检查长度) if config.get("DEEPSEEK_API_KEY"): api_key = config.get("DEEPSEEK_API_KEY", "") if len(api_key) < 20: return False, "DeepSeek API Key格式不正确(长度太短)" - + return True, "配置验证通过" def reload_config(self): diff --git a/env_example.txt b/env_example.txt index 2549f5b2..fdc9952e 100644 --- a/env_example.txt +++ b/env_example.txt @@ -7,6 +7,12 @@ DEEPSEEK_API_KEY=your_deepseek_api_key_here DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +# OrcaRouter API配置(可选,设置了 ORCAROUTER_API_KEY 后优先使用 OrcaRouter 引擎) +# OrcaRouter:https://www.orcarouter.ai | API:https://api.orcarouter.ai/v1 +ORCAROUTER_API_KEY= +ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1 +ORCAROUTER_MODEL=orcarouter/auto + # Tushare数据源配置(备用数据源) TUSHARE_TOKEN=your_tushare_token_here diff --git a/llm_client.py b/llm_client.py new file mode 100644 index 00000000..5a4e0881 --- /dev/null +++ b/llm_client.py @@ -0,0 +1,21 @@ +""" +LLM客户端工厂 + +根据 .env 配置自动选择 AI 引擎: +- 设置了 ORCAROUTER_API_KEY → 使用 OrcaRouter(命名 OpenAI 兼容网关) +- 否则回落到 DeepSeek(原有默认引擎) +""" +import config +from deepseek_client import DeepSeekClient + + +def get_llm_client(model=None): + """返回当前配置对应的 LLM 客户端。 + + Args: + model: 可选的模型名称覆盖,默认从对应 provider 的配置读取。 + """ + if config.ORCAROUTER_API_KEY: + from orcarouter_client import OrcaRouterClient + return OrcaRouterClient(model=model) + return DeepSeekClient(model=model) diff --git a/longhubang_agents.py b/longhubang_agents.py index e374374c..fbe64a60 100644 --- a/longhubang_agents.py +++ b/longhubang_agents.py @@ -3,7 +3,7 @@ 专注于龙虎榜数据的多维度分析 """ -from deepseek_client import DeepSeekClient +from llm_client import get_llm_client from typing import Dict, Any, List import time import config @@ -11,10 +11,10 @@ class LonghubangAgents: """龙虎榜AI分析师集合""" - + def __init__(self, model=None): self.model = model or config.DEFAULT_MODEL_NAME - self.deepseek_client = DeepSeekClient(model=self.model) + self.deepseek_client = get_llm_client(model=model) print(f"[智瞰龙虎] AI分析师系统初始化 (模型: {self.model})") def youzi_behavior_analyst(self, longhubang_data: str, summary: Dict) -> Dict[str, Any]: diff --git a/macro_analysis_agents.py b/macro_analysis_agents.py index d530b1b7..9156d65f 100644 --- a/macro_analysis_agents.py +++ b/macro_analysis_agents.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List import config -from deepseek_client import DeepSeekClient +from llm_client import get_llm_client class MacroAnalysisAgents: @@ -18,7 +18,7 @@ class MacroAnalysisAgents: def __init__(self, model: str | None = None) -> None: self.model = model or config.DEFAULT_MODEL_NAME - self.client = DeepSeekClient(model=self.model) + self.client = get_llm_client(model=model) def macro_analyst_agent(self, context_text: str) -> Dict[str, Any]: prompt = f""" diff --git a/macro_cycle_agents.py b/macro_cycle_agents.py index 8661bb69..9c7e406e 100644 --- a/macro_cycle_agents.py +++ b/macro_cycle_agents.py @@ -3,7 +3,7 @@ 包含四位专业分析师:康波周期分析师、美林时钟分析师、中国政策分析师、首席宏观策略师 """ -from deepseek_client import DeepSeekClient +from llm_client import get_llm_client from typing import Dict, Any import time import config @@ -14,7 +14,7 @@ class MacroCycleAgents: def __init__(self, model=None): self.model = model or config.DEFAULT_MODEL_NAME - self.deepseek_client = DeepSeekClient(model=self.model) + self.deepseek_client = get_llm_client(model=model) print(f"[宏观周期] AI智能体系统初始化 (模型: {self.model})") def kondratieff_wave_agent(self, macro_data_text: str) -> Dict[str, Any]: diff --git a/main_force_analysis.py b/main_force_analysis.py index de20288a..a194e615 100644 --- a/main_force_analysis.py +++ b/main_force_analysis.py @@ -10,7 +10,6 @@ from main_force_selector import main_force_selector from stock_data import StockDataFetcher from ai_agents import StockAnalysisAgents -from deepseek_client import DeepSeekClient import time import json import config diff --git a/model_config.py b/model_config.py index c9a01f9f..bdb8d781 100644 --- a/model_config.py +++ b/model_config.py @@ -24,6 +24,7 @@ "moonshotai/Kimi-K2-Instruct-0905": "Kimi (硅基流动)", "Ring-1T": "蚂蚁百灵 (硅基流动)", "step3": "阶跃星辰(硅基流动)", + "orcarouter/auto": "OrcaRouter 自动路由", } # 获取 .env 中配置的默认模型名称 diff --git a/news_flow_agents.py b/news_flow_agents.py index 299c31d0..972dca58 100644 --- a/news_flow_agents.py +++ b/news_flow_agents.py @@ -29,13 +29,13 @@ def __init__(self, model: str = None): self._init_client() def _init_client(self): - """初始化DeepSeek客户端""" + """初始化AI客户端(OrcaRouter 或 DeepSeek)""" try: - from deepseek_client import DeepSeekClient - self.deepseek_client = DeepSeekClient(model=self.model) - logger.info(f"✅ DeepSeek客户端初始化成功,模型: {self.model}") + from llm_client import get_llm_client + self.deepseek_client = get_llm_client(model=self.model) + logger.info(f"✅ AI客户端初始化成功,模型: {self.model}") except Exception as e: - logger.error(f"❌ DeepSeek客户端初始化失败: {e}") + logger.error(f"❌ AI客户端初始化失败: {e}") self.deepseek_client = None def is_available(self) -> bool: diff --git a/orcarouter_client.py b/orcarouter_client.py new file mode 100644 index 00000000..8532a666 --- /dev/null +++ b/orcarouter_client.py @@ -0,0 +1,23 @@ +import openai +from deepseek_client import DeepSeekClient +import config + + +class OrcaRouterClient(DeepSeekClient): + """OrcaRouter API客户端 + + 基于 DeepSeekClient,复用其全部技术面/基本面/资金面分析 prompt 与 + chat/completions 调用逻辑,仅替换为 OrcaRouter 的 OpenAI 兼容网关配置。 + """ + + # 系统的 DeepSeek 默认占位模型名:视为"未显式指定模型" + _deepseek_default_placeholder = "deepseek-chat" + + def __init__(self, model=None): + if not model or model == self._deepseek_default_placeholder: + model = config.ORCAROUTER_MODEL + self.model = model + self.client = openai.OpenAI( + api_key=config.ORCAROUTER_API_KEY, + base_url=config.ORCAROUTER_BASE_URL + ) diff --git a/sector_strategy_agents.py b/sector_strategy_agents.py index 6b59e76c..bb263991 100644 --- a/sector_strategy_agents.py +++ b/sector_strategy_agents.py @@ -3,7 +3,7 @@ 包含四个专业分析师智能体 """ -from deepseek_client import DeepSeekClient +from llm_client import get_llm_client from typing import Dict, Any import time import config @@ -11,10 +11,10 @@ class SectorStrategyAgents: """板块策略AI智能体集合""" - + def __init__(self, model=None): self.model = model or config.DEFAULT_MODEL_NAME - self.deepseek_client = DeepSeekClient(model=self.model) + self.deepseek_client = get_llm_client(model=model) print(f"[智策] AI智能体系统初始化 (模型: {self.model})") def macro_strategist_agent(self, market_data: Dict, news_data: list) -> Dict[str, Any]: diff --git a/sector_strategy_engine.py b/sector_strategy_engine.py index f1daecd7..4748a03f 100644 --- a/sector_strategy_engine.py +++ b/sector_strategy_engine.py @@ -5,7 +5,7 @@ from sector_strategy_agents import SectorStrategyAgents from sector_strategy_db import SectorStrategyDatabase -from deepseek_client import DeepSeekClient +from llm_client import get_llm_client from typing import Dict, Any import time import json @@ -16,11 +16,11 @@ class SectorStrategyEngine: """板块策略综合研判引擎""" - + def __init__(self, model=None): self.model = model or config.DEFAULT_MODEL_NAME self.agents = SectorStrategyAgents(model=self.model) - self.deepseek_client = DeepSeekClient(model=self.model) + self.deepseek_client = get_llm_client(model=model) self.database = SectorStrategyDatabase() self.logger = logging.getLogger(__name__) if not self.logger.handlers: diff --git a/smart_monitor_deepseek.py b/smart_monitor_deepseek.py index 625674a8..9c3d5e21 100644 --- a/smart_monitor_deepseek.py +++ b/smart_monitor_deepseek.py @@ -15,15 +15,23 @@ class SmartMonitorDeepSeek: def __init__(self, api_key: str): """ - 初始化DeepSeek客户端 - + 初始化AI客户端(OrcaRouter 优先,否则 DeepSeek) + Args: - api_key: DeepSeek API密钥 + api_key: DeepSeek API密钥(未配置 OrcaRouter 时使用) """ - self.api_key = api_key - self.base_url = config.DEEPSEEK_BASE_URL + if config.ORCAROUTER_API_KEY: + self.api_key = config.ORCAROUTER_API_KEY + self.base_url = config.ORCAROUTER_BASE_URL + self.default_model = config.ORCAROUTER_MODEL + self.provider_name = "OrcaRouter" + else: + self.api_key = api_key + self.base_url = config.DEEPSEEK_BASE_URL + self.default_model = config.DEFAULT_MODEL_NAME + self.provider_name = "DeepSeek" self.headers = { - "Authorization": f"Bearer {api_key}", + "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" } self.logger = logging.getLogger(__name__) @@ -155,7 +163,7 @@ def chat_completion(self, messages: List[Dict], model: str = None, """ import requests - model = model or config.DEFAULT_MODEL_NAME + model = model or self.default_model payload = { "model": model, diff --git a/test_orcarouter_client.py b/test_orcarouter_client.py new file mode 100644 index 00000000..e8b8bb7f --- /dev/null +++ b/test_orcarouter_client.py @@ -0,0 +1,101 @@ +""" +测试 OrcaRouter LLM 客户端工厂 + +运行方式: + # 无需 API key,单元测试全部通过 + python3 -m pytest test_orcarouter_client.py -v + +覆盖: + - 设置了 ORCAROUTER_API_KEY 时,工厂返回 OrcaRouterClient + - 未设置 ORCAROUTER_API_KEY 时,工厂返回 DeepSeekClient(原有默认行为) + - OrcaRouterClient 使用 OrcaRouter 的 base_url 与模型 +""" + +import pytest +import sys +import os +from unittest.mock import patch + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# Stub dotenv before importing config +import types +_dotenv = types.ModuleType("dotenv") +_dotenv.load_dotenv = lambda *a, **k: None +sys.modules["dotenv"] = _dotenv + + +@pytest.fixture(autouse=True) +def clean_env(): + """隔离 ORCAROUTER_API_KEY 环境变量""" + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("ORCAROUTER_API_KEY", None) + os.environ.pop("ORCAROUTER_BASE_URL", None) + os.environ.pop("ORCAROUTER_MODEL", None) + yield + + +def test_factory_returns_orcarouter_when_configured(): + import config + from llm_client import get_llm_client + + with patch.object(config, "ORCAROUTER_API_KEY", "sk-orca-test-123"): + client = get_llm_client() + from orcarouter_client import OrcaRouterClient + assert isinstance(client, OrcaRouterClient) + assert client.client.base_url == "https://api.orcarouter.ai/v1/" + assert client.model == "orcarouter/auto" + + +def test_factory_returns_deepseek_by_default(): + import config + from llm_client import get_llm_client + + with patch.object(config, "ORCAROUTER_API_KEY", ""), \ + patch.object(config, "DEEPSEEK_API_KEY", "sk-deepseek-test-123"): + client = get_llm_client() + from deepseek_client import DeepSeekClient + assert isinstance(client, DeepSeekClient) + + +def test_orcarouter_default_base_url_and_model(): + import config + + assert config.ORCAROUTER_BASE_URL == "https://api.orcarouter.ai/v1" + assert config.ORCAROUTER_MODEL == "orcarouter/auto" + + +def test_orcarouter_custom_model_passed_through(): + import config + from llm_client import get_llm_client + + with patch.object(config, "ORCAROUTER_API_KEY", "sk-orca-test-123"): + client = get_llm_client(model="deepseek/deepseek-v4-pro") + assert client.model == "deepseek/deepseek-v4-pro" + + +def test_orcarouter_maps_deepseek_default_placeholder(): + """显式传入 DeepSeek 默认占位模型名时,应落到 OrcaRouter 默认模型。""" + import config + from llm_client import get_llm_client + + with patch.object(config, "ORCAROUTER_API_KEY", "sk-orca-test-123"): + client = get_llm_client(model="deepseek-chat") + assert client.model == "orcarouter/auto" + + +def test_config_manager_accepts_orcarouter_only(): + """仅配置 OrcaRouter 密钥时,DeepSeek 密钥不再必填。""" + from config_manager import config_manager + + ok, msg = config_manager.validate_config({ + "DEEPSEEK_API_KEY": "", + "ORCAROUTER_API_KEY": "sk-orca-1234567890", + }) + assert ok is True + + ok, msg = config_manager.validate_config({ + "DEEPSEEK_API_KEY": "sk-deepseek-1234567890", + "ORCAROUTER_API_KEY": "", + }) + assert ok is True diff --git a/update_env_example.py b/update_env_example.py index 9a054dc5..1fed1212 100644 --- a/update_env_example.py +++ b/update_env_example.py @@ -19,6 +19,14 @@ DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +# ========== OrcaRouter API配置(可选)========== +# 设置了 ORCAROUTER_API_KEY 后,系统将优先使用 OrcaRouter 作为 AI 引擎 +# OrcaRouter:https://www.orcarouter.ai | API:https://api.orcarouter.ai/v1 +ORCAROUTER_API_KEY= +ORCAROUTER_BASE_URL=https://api.orcarouter.ai/v1 +ORCAROUTER_MODEL=orcarouter/auto + + # ========== Tushare数据接口(可选)========== # Tushare Token(可选,用于获取更多金融数据) # 获取地址:https://tushare.pro/register