{"repo": "aidocplus/dragonstock", "files": {"backend/app/services/llm/llm_service.py": {"33297c5dc6145856df0f0852bcc704f784cf14c2": {"t": 1785746497.640697, "n": 20797, "h": "ada5b13d85d122da", "c": "\"\"\"\n统一LLM服务 - 单例模式,管理所有提供商切换与配置持久化\n\"\"\"\nimport json\nimport logging\nimport sqlite3\nfrom typing import Any, AsyncIterator, Dict, List, Optional\n\nfrom app.services.llm.base import (\n LLMProvider, LLMMessage, LLMToolDef, LLMResponse,\n ModelInfo, ProviderConfig,\n)\nfrom app.services.llm.openai_provider import OpenAICompatibleProvider\nfrom app.services.database_service import database_service\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================\n# 内置提供商预设(截至2026.03调研)\n# ============================================================\nBUILTIN_PROVIDERS: Dict[str, Dict[str, Any]] = {\n \"openai\": {\n \"display_name\": \"OpenAI\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"needs_proxy\": True,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.openai.com/api-keys\",\n \"models\": [\n {\"id\": \"gpt-4o\", \"name\": \"GPT-4o\", \"type\": \"chat\"},\n {\"id\": \"gpt-4o-mini\", \"name\": \"GPT-4o Mini\", \"type\": \"lightweight\"},\n {\"id\": \"gpt-4.1\", \"name\": \"GPT-4.1(代码优化)\", \"type\": \"chat\"},\n {\"id\": \"gpt-4.1-mini\", \"name\": \"GPT-4.1 Mini\", \"type\": \"lightweight\"},\n {\"id\": \"gpt-4.1-nano\", \"name\": \"GPT-4.1 Nano\", \"type\": \"lightweight\"},\n {\"id\": \"o3\", \"name\": \"o3(深度推理)\", \"type\": \"reasoning\"},\n {\"id\": \"o4-mini\", \"name\": \"o4-mini(快速推理)\", \"type\": \"reasoning\"},\n {\"id\": \"gpt-5\", \"name\": \"GPT-5\", \"type\": \"chat\"},\n {\"id\": \"gpt-5-mini\", \"name\": \"GPT-5 Mini\", \"type\": \"lightweight\"},\n ],\n },\n \"gemini\": {\n \"display_name\": \"Google Gemini\",\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"needs_proxy\": True,\n \"supports_tools\": True,\n \"api_key_url\": \"https://aistudio.google.com/apikey\",\n \"models\": [\n {\"id\": \"gemini-3-pro-preview\", \"name\": \"Gemini 3 Pro(预览)\", \"type\": \"chat\"},\n {\"id\": \"gemini-3-flash-preview\", \"name\": \"Gemini 3 Flash(预览)\", \"type\": \"chat\"},\n {\"id\": \"gemini-2.5-pro\", \"name\": \"Gemini 2.5 Pro\", \"type\": \"chat\"},\n {\"id\": \"gemini-2.5-flash\", \"name\": \"Gemini 2.5 Flash(稳定推荐)\", \"type\": \"chat\"},\n {\"id\": \"gemini-2.5-flash-lite\", \"name\": \"Gemini 2.5 Flash Lite\", \"type\": \"lightweight\"},\n {\"id\": \"gemini-2.0-flash\", \"name\": \"Gemini 2.0 Flash\", \"type\": \"lightweight\"},\n ],\n },\n \"xai\": {\n \"display_name\": \"xAI (Grok)\",\n \"base_url\": \"https://api.x.ai/v1\",\n \"needs_proxy\": True,\n \"supports_tools\": True,\n \"api_key_url\": \"https://console.x.ai/\",\n \"models\": [\n {\"id\": \"grok-4-0709\", \"name\": \"Grok 4\", \"type\": \"chat\"},\n {\"id\": \"grok-3\", \"name\": \"Grok 3\", \"type\": \"chat\"},\n {\"id\": \"grok-3-mini\", \"name\": \"Grok 3 Mini\", \"type\": \"lightweight\"},\n ],\n },\n \"deepseek\": {\n \"display_name\": \"DeepSeek\",\n \"base_url\": \"https://api.deepseek.com\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.deepseek.com/api_keys\",\n \"models\": [\n {\"id\": \"deepseek-chat\", \"name\": \"DeepSeek-V3.2(通用)\", \"type\": \"chat\"},\n {\"id\": \"deepseek-reasoner\", \"name\": \"DeepSeek-R1(推理)\", \"type\": \"reasoning\"},\n ],\n },\n \"qwen\": {\n \"display_name\": \"通义千问 (Qwen)\",\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://bailian.console.aliyun.com/#/api-key\",\n \"models\": [\n {\"id\": \"qwen3-max\", \"name\": \"Qwen3 Max(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"qwen-max\", \"name\": \"Qwen Max\", \"type\": \"chat\"},\n {\"id\": \"qwen-plus\", \"name\": \"Qwen Plus(推荐)\", \"type\": \"chat\"},\n {\"id\": \"qwen-turbo\", \"name\": \"Qwen Turbo\", \"type\": \"lightweight\"},\n {\"id\": \"qwen-long\", \"name\": \"Qwen Long\", \"type\": \"chat\"},\n {\"id\": \"qwen3-coder-plus\", \"name\": \"Qwen3 Coder Plus(编码)\", \"type\": \"coding\"},\n {\"id\": \"qwen-coder-plus\", \"name\": \"Qwen Coder Plus\", \"type\": \"coding\"},\n {\"id\": \"qwq-plus\", \"name\": \"QwQ Plus(推理)\", \"type\": \"reasoning\"},\n {\"id\": \"qwen-flash\", \"name\": \"Qwen Flash\", \"type\": \"lightweight\"},\n ],\n },\n \"glm\": {\n \"display_name\": \"智谱 AI(通用)\",\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://open.bigmodel.cn/usercenter/apikeys\",\n \"models\": [\n {\"id\": \"glm-5\", \"name\": \"GLM-5(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"glm-4.7\", \"name\": \"GLM-4.7\", \"type\": \"chat\"},\n {\"id\": \"glm-4.7-flash\", \"name\": \"GLM-4.7 Flash(免费)\", \"type\": \"lightweight\"},\n {\"id\": \"glm-4.6\", \"name\": \"GLM-4.6\", \"type\": \"chat\"},\n {\"id\": \"glm-4.5-air\", \"name\": \"GLM-4.5 Air\", \"type\": \"chat\"},\n {\"id\": \"glm-4.5-airx\", \"name\": \"GLM-4.5 AirX\", \"type\": \"chat\"},\n {\"id\": \"glm-4.5-flash\", \"name\": \"GLM-4.5 Flash(免费)\", \"type\": \"lightweight\"},\n {\"id\": \"glm-4-flash\", \"name\": \"GLM-4 Flash(免费)\", \"type\": \"lightweight\"},\n {\"id\": \"glm-4-long\", \"name\": \"GLM-4 Long\", \"type\": \"chat\"},\n ],\n },\n \"glm-code\": {\n \"display_name\": \"智谱 AI(Coding Plan)\",\n \"base_url\": \"https://open.bigmodel.cn/api/coding/paas/v4\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://open.bigmodel.cn/usercenter/apikeys\",\n \"share_api_key_with\": \"glm\",\n \"models\": [\n {\"id\": \"GLM-5\", \"name\": \"GLM-5(旗舰)\", \"type\": \"coding\"},\n {\"id\": \"GLM-4.7\", \"name\": \"GLM-4.7\", \"type\": \"coding\"},\n {\"id\": \"GLM-4.6\", \"name\": \"GLM-4.6\", \"type\": \"coding\"},\n {\"id\": \"GLM-4.5-air\", \"name\": \"GLM-4.5 Air\", \"type\": \"coding\"},\n ],\n },\n \"minimax\": {\n \"display_name\": \"MiniMax(通用)\",\n \"base_url\": \"https://api.minimaxi.com/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.minimaxi.com/dashboard\",\n \"models\": [\n {\"id\": \"MiniMax-M2.5\", \"name\": \"MiniMax M2.5(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"MiniMax-M2.5-highspeed\", \"name\": \"MiniMax M2.5 高速版\", \"type\": \"lightweight\"},\n {\"id\": \"MiniMax-M2.1\", \"name\": \"MiniMax M2.1\", \"type\": \"chat\"},\n {\"id\": \"MiniMax-M2.1-highspeed\", \"name\": \"MiniMax M2.1 高速版\", \"type\": \"lightweight\"},\n ],\n },\n \"minimax-code\": {\n \"display_name\": \"MiniMax(Coding Plan)\",\n \"base_url\": \"https://api.minimaxi.com/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.minimaxi.com/dashboard\",\n \"share_api_key_with\": \"minimax\",\n \"models\": [\n {\"id\": \"MiniMax-M2.5\", \"name\": \"MiniMax M2.5\", \"type\": \"coding\"},\n {\"id\": \"MiniMax-M2.5-highspeed\", \"name\": \"MiniMax M2.5 高速版\", \"type\": \"coding\"},\n ],\n },\n \"kimi\": {\n \"display_name\": \"Kimi / Moonshot 开放平台\",\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.moonshot.cn/console/api-keys\",\n \"models\": [\n {\"id\": \"kimi-k2.5\", \"name\": \"Kimi K2.5(旗舰多模态)\", \"type\": \"chat\"},\n {\"id\": \"kimi-k2\", \"name\": \"Kimi K2\", \"type\": \"chat\"},\n {\"id\": \"kimi-latest\", \"name\": \"Kimi Latest\", \"type\": \"chat\"},\n {\"id\": \"moonshot-v1-auto\", \"name\": \"Moonshot v1 Auto\", \"type\": \"chat\"},\n {\"id\": \"moonshot-v1-128k\", \"name\": \"Moonshot v1 128K\", \"type\": \"chat\"},\n ],\n },\n \"kimi-code\": {\n \"display_name\": \"Kimi Code(会员编程)\",\n \"base_url\": \"https://api.kimi.com/coding/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.moonshot.cn/console/api-keys\",\n \"share_api_key_with\": \"kimi\",\n \"models\": [\n {\"id\": \"kimi-for-coding\", \"name\": \"Kimi for Coding(默认)\", \"type\": \"coding\"},\n ],\n },\n \"doubao\": {\n \"display_name\": \"豆包 (Doubao)\",\n \"base_url\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey\",\n \"models\": [\n {\"id\": \"doubao-seed-2-0-pro\", \"name\": \"Doubao Seed 2.0 Pro(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"doubao-seed-2-0-code\", \"name\": \"Doubao Seed 2.0 Code(编码)\", \"type\": \"coding\"},\n {\"id\": \"doubao-seed-2-0-lite\", \"name\": \"Doubao Seed 2.0 Lite(轻量)\", \"type\": \"lightweight\"},\n ],\n },\n \"ollama\": {\n \"display_name\": \"Ollama(本地)\",\n \"base_url\": \"http://localhost:11434/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": False,\n \"api_key_url\": \"https://ollama.com/download\",\n \"models\": [], # 动态检测\n },\n \"custom\": {\n \"display_name\": \"自定义(OpenAI兼容)\",\n \"base_url\": \"\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"\",\n \"models\": [],\n },\n}\n\n\nclass LLMService:\n \"\"\"统一LLM服务单例\"\"\"\n\n _instance: Optional[\"LLMService\"] = None\n\n def __new__(cls) -> \"LLMService\":\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n cls._instance._initialized = False\n return cls._instance\n\n def __init__(self):\n if self._initialized:\n return\n self._initialized = True\n self._provider: Optional[LLMProvider] = None\n self._active_provider_name: str = \"\"\n self._db_path: str = \"\"\n logger.info(\"LLMService 初始化完成\")\n\n def set_db_path(self, db_path: str):\n \"\"\"设置数据库路径并初始化llm_settings表\"\"\"\n self._db_path = db_path\n self._init_db()\n self._load_provider_from_db()\n\n # ============================================================\n # 数据库操作\n # ============================================================\n def _init_db(self):\n if not self._db_path:\n return\n try:\n with database_service.get_connection() as conn:\n conn.execute(\"\"\"\n CREATE TABLE IF NOT EXISTS llm_settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now', 'localtime'))\n )\n \"\"\")\n conn.commit()\n except Exception as e:\n logger.error(\"初始化llm_settings表失败: %s\", e)\n\n def _get_setting(self, key: str, default: str = \"\") -> str:\n if not self._db_path:\n return default\n try:\n with database_service.get_connection() as conn:\n row = conn.execute(\n \"SELECT value FROM llm_settings WHERE key = ?\", (key,)\n ).fetchone()\n return row[0] if row else default\n except Exception:\n return default\n\n def _set_setting(self, key: str, value: str):\n if not self._db_path:\n return\n try:\n with database_service.get_connection() as conn:\n conn.execute(\"\"\"\n INSERT INTO llm_settings (key, value, updated_at)\n VALUES (?, ?, datetime('now', 'localtime'))\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n updated_at = excluded.updated_at\n \"\"\", (key, value))\n conn.commit()\n except Exception as e:\n logger.error(\"保存LLM设置失败 [%s]: %s\", key, e)\n\n # ============================================================\n # Provider管理\n # ============================================================\n def _load_provider_from_db(self):\n \"\"\"从数据库加载已保存的provider配置\"\"\"\n provider_name = self._get_setting(\"active_provider\", \"\")\n if not provider_name:\n return\n try:\n self._build_provider(provider_name)\n except Exception as e:\n logger.warning(\"加载已保存的LLM provider失败: %s\", e)\n\n def _build_provider(self, provider_name: str):\n \"\"\"根据provider名称构建Provider实例\"\"\"\n if provider_name not in BUILTIN_PROVIDERS:\n raise ValueError(f\"未知的LLM提供商: {provider_name}\")\n\n preset = BUILTIN_PROVIDERS[provider_name]\n\n api_key = self._get_setting(f\"{provider_name}_api_key\", \"\")\n # 支持 share_api_key_with:Coding Plan 提供商可共享主提供商的 API Key\n if not api_key and preset.get(\"share_api_key_with\"):\n api_key = self._get_setting(f\"{preset['share_api_key_with']}_api_key\", \"\")\n model = self._get_setting(f\"{provider_name}_model\", \"\")\n custom_base_url = self._get_setting(f\"{provider_name}_base_url\", \"\")\n proxy_url = self._get_setting(\"proxy_url\", \"\")\n proxy_enabled = self._get_setting(\"proxy_enabled\", \"false\") == \"true\"\n temperature = float(self._get_setting(\"temperature\", \"0.7\"))\n max_tokens = int(self._get_setting(\"max_tokens\", \"0\"))\n\n base_url = custom_base_url or preset[\"base_url\"]\n\n # 对非Ollama提供商,没有API Key则不创建\n if provider_name not in (\"ollama\", \"custom\") and not api_key:\n logger.info(\"提供商 %s 未配置API Key,跳过\", provider_name)\n return\n\n # 默认选第一个模型\n if not model and preset[\"models\"]:\n model = preset[\"models\"][0][\"id\"]\n\n models = [\n ModelInfo(id=m[\"id\"], name=m[\"name\"], model_type=m.get(\"type\", \"chat\"))\n for m in preset[\"models\"]\n ]\n\n # 代理由用户全局开关控制\n effective_proxy = proxy_url if (proxy_enabled and proxy_url) else None\n\n config = ProviderConfig(\n provider_name=provider_name,\n display_name=preset[\"display_name\"],\n base_url=base_url,\n api_key=api_key,\n model=model,\n proxy=effective_proxy,\n temperature=temperature,\n max_tokens=max_tokens,\n models=models,\n supports_tools=preset.get(\"supports_tools\", True),\n needs_proxy=preset.get(\"needs_proxy\", False),\n api_key_url=preset.get(\"api_key_url\", \"\"),\n )\n\n self._provider = OpenAICompatibleProvider(config)\n self._active_provider_name = provider_name\n logger.info(\"LLM Provider已切换: %s / %s\", provider_name, model)\n\n def switch_provider(self, provider_name: str, config: Dict[str, Any]):\n \"\"\"切换提供商并保存配置\"\"\"\n if config.get(\"api_key\"):\n self._set_setting(f\"{provider_name}_api_key\", config[\"api_key\"])\n if config.get(\"model\"):\n self._set_setting(f\"{provider_name}_model\", config[\"model\"])\n if config.get(\"base_url\"):\n self._set_setting(f\"{provider_name}_base_url\", config[\"base_url\"])\n if config.get(\"proxy_url\") is not None:\n self._set_setting(\"proxy_url\", config[\"proxy_url\"])\n if config.get(\"proxy_enabled\") is not None:\n self._set_setting(\"proxy_enabled\", \"true\" if config[\"proxy_enabled\"] else \"false\")\n if config.get(\"temperature\") is not None:\n self._set_setting(\"temperature\", str(config[\"temperature\"]))\n if config.get(\"max_tokens\") is not None:\n self._set_setting(\"max_tokens\", str(config[\"max_tokens\"]))\n\n self._set_setting(\"active_provider\", provider_name)\n self._build_provider(provider_name)\n\n # ============================================================\n # 对外接口\n # ============================================================\n @property\n def is_configured(self) -> bool:\n return self._provider is not None\n\n @property\n def active_provider_name(self) -> str:\n return self._active_provider_name\n\n def get_provider(self) -> Optional[LLMProvider]:\n return self._provider\n\n async def chat(\n self,\n messages: List[LLMMessage],\n tools: Optional[List[LLMToolDef]] = None,\n stream: bool = False,\n temperature: Optional[float] = None,\n max_tokens: Optional[int] = None,\n ) -> LLMResponse:\n if not self._provider:\n raise RuntimeError(\"LLM服务未配置,请先在设置中选择并配置AI模型提供商\")\n return await self._provider.chat(messages, tools, stream, temperature, max_tokens)\n\n async def chat_stream(\n self,\n messages: List[LLMMessage],\n tools: Optional[List[LLMToolDef]] = None,\n temperature: Optional[float] = None,\n max_tokens: Optional[int] = None,\n ) -> AsyncIterator[str]:\n if not self._provider:\n raise RuntimeError(\"LLM服务未配置,请先在设置中选择并配置AI模型提供商\")\n async for chunk in self._provider.chat_stream(messages, tools, temperature, max_tokens):\n yield chunk\n\n async def test_connection(self) -> Dict[str, Any]:\n if not self._provider:\n return {\"success\": False, \"message\": \"LLM服务未配置\"}\n return await self._provider.test_connection()\n\n async def list_models(self, provider_name: Optional[str] = None) -> List[ModelInfo]:\n \"\"\"获取指定提供商或当前提供商的模型列表\"\"\"\n if provider_name and provider_name == \"ollama\":\n # Ollama需要动态检测\n temp_config = ProviderConfig(\n provider_name=\"ollama\",\n display_name=\"Ollama\",\n base_url=self._get_setting(\"ollama_base_url\", \"http://localhost:11434/v1\"),\n api_key=\"ollama\",\n model=\"\",\n )\n temp_provider = OpenAICompatibleProvider(temp_config)\n models = await temp_provider.list_models()\n await temp_provider.close()\n return models\n\n if provider_name and provider_name in BUILTIN_PROVIDERS:\n preset = BUILTIN_PROVIDERS[provider_name]\n return [\n ModelInfo(id=m[\"id\"], name=m[\"name\"], model_type=m.get(\"type\", \"chat\"))\n for m in preset[\"models\"]\n ]\n\n if self._provider:\n return await self._provider.list_models()\n return []\n\n def get_settings(self) -> Dict[str, Any]:\n \"\"\"获取当前所有LLM设置\"\"\"\n settings: Dict[str, Any] = {\n \"active_provider\": self._get_setting(\"active_provider\", \"\"),\n \"proxy_url\": self._get_setting(\"proxy_url\", \"\"),\n \"proxy_enabled\": self._get_setting(\"proxy_enabled\", \"false\") == \"true\",\n \"temperature\": float(self._get_setting(\"temperature\", \"0.7\")),\n \"max_tokens\": int(self._get_setting(\"max_tokens\", \"0\")),\n }\n\n # 各提供商独立配置\n for name in BUILTIN_PROVIDERS:\n settings[f\"{name}_api_key\"] = \"***\" if self._get_setting(f\"{name}_api_key\") else \"\"\n settings[f\"{name}_model\"] = self._get_setting(f\"{name}_model\", \"\")\n settings[f\"{name}_base_url\"] = self._get_setting(f\"{name}_base_url\", \"\")\n\n return settings\n\n def get_providers_info(self) -> List[Dict[str, Any]]:\n \"\"\"获取全部可用提供商信息\"\"\"\n providers = []\n for name, preset in BUILTIN_PROVIDERS.items():\n has_key = bool(self._get_setting(f\"{name}_api_key\"))\n # Coding Plan 提供商可共享主提供商的 API Key\n share_with = preset.get(\"share_api_key_with\", \"\")\n if not has_key and share_with:\n has_key = bool(self._get_setting(f\"{share_with}_api_key\"))\n selected_model = self._get_setting(f\"{name}_model\", \"\")\n custom_base_url = self._get_setting(f\"{name}_base_url\", \"\")\n providers.append({\n \"name\": name,\n \"display_name\": preset[\"display_name\"],\n \"base_url\": preset[\"base_url\"],\n \"custom_base_url\": custom_base_url,\n \"needs_proxy\": preset.get(\"needs_proxy\", False),\n \"supports_tools\": preset.get(\"supports_tools\", True),\n \"api_key_url\": preset.get(\"api_key_url\", \"\"),\n \"has_api_key\": has_key,\n \"selected_model\": selected_model,\n \"is_active\": name == self._active_provider_name,\n \"models\": preset[\"models\"],\n \"share_api_key_with\": share_with,\n })\n return providers\n\n def save_settings(self, settings: Dict[str, Any]):\n \"\"\"批量保存设置\"\"\"\n for key, value in settings.items():\n if key.endswith(\"_api_key\") and value == \"***\":\n continue # 跳过未修改的密钥\n self._set_setting(key, str(value))\n\n # 如果active_provider变了,重建provider\n new_provider = settings.get(\"active_provider\", \"\")\n if new_provider and new_provider != self._active_provider_name:\n try:\n self._build_provider(new_provider)\n except Exception as e:\n logger.error(\"切换provider失败: %s\", e)\n\n\n# 全局单例\nllm_service = LLMService()\n"}, "53ac1ac7eb7feb5c2e626742969c856f9a1dc66b": {"t": 1785764975.4044533, "n": 20735, "h": "ec603eda193f7042", "c": "\"\"\"\n统一LLM服务 - 单例模式,管理所有提供商切换与配置持久化\n\"\"\"\nimport json\nimport logging\nimport sqlite3\nfrom typing import Any, AsyncIterator, Dict, List, Optional\n\nfrom app.services.llm.base import (\n LLMProvider, LLMMessage, LLMToolDef, LLMResponse,\n ModelInfo, ProviderConfig,\n)\nfrom app.services.llm.openai_provider import OpenAICompatibleProvider\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================\n# 内置提供商预设(截至2026.03调研)\n# ============================================================\nBUILTIN_PROVIDERS: Dict[str, Dict[str, Any]] = {\n \"openai\": {\n \"display_name\": \"OpenAI\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"needs_proxy\": True,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.openai.com/api-keys\",\n \"models\": [\n {\"id\": \"gpt-4o\", \"name\": \"GPT-4o\", \"type\": \"chat\"},\n {\"id\": \"gpt-4o-mini\", \"name\": \"GPT-4o Mini\", \"type\": \"lightweight\"},\n {\"id\": \"gpt-4.1\", \"name\": \"GPT-4.1(代码优化)\", \"type\": \"chat\"},\n {\"id\": \"gpt-4.1-mini\", \"name\": \"GPT-4.1 Mini\", \"type\": \"lightweight\"},\n {\"id\": \"gpt-4.1-nano\", \"name\": \"GPT-4.1 Nano\", \"type\": \"lightweight\"},\n {\"id\": \"o3\", \"name\": \"o3(深度推理)\", \"type\": \"reasoning\"},\n {\"id\": \"o4-mini\", \"name\": \"o4-mini(快速推理)\", \"type\": \"reasoning\"},\n {\"id\": \"gpt-5\", \"name\": \"GPT-5\", \"type\": \"chat\"},\n {\"id\": \"gpt-5-mini\", \"name\": \"GPT-5 Mini\", \"type\": \"lightweight\"},\n ],\n },\n \"gemini\": {\n \"display_name\": \"Google Gemini\",\n \"base_url\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n \"needs_proxy\": True,\n \"supports_tools\": True,\n \"api_key_url\": \"https://aistudio.google.com/apikey\",\n \"models\": [\n {\"id\": \"gemini-3-pro-preview\", \"name\": \"Gemini 3 Pro(预览)\", \"type\": \"chat\"},\n {\"id\": \"gemini-3-flash-preview\", \"name\": \"Gemini 3 Flash(预览)\", \"type\": \"chat\"},\n {\"id\": \"gemini-2.5-pro\", \"name\": \"Gemini 2.5 Pro\", \"type\": \"chat\"},\n {\"id\": \"gemini-2.5-flash\", \"name\": \"Gemini 2.5 Flash(稳定推荐)\", \"type\": \"chat\"},\n {\"id\": \"gemini-2.5-flash-lite\", \"name\": \"Gemini 2.5 Flash Lite\", \"type\": \"lightweight\"},\n {\"id\": \"gemini-2.0-flash\", \"name\": \"Gemini 2.0 Flash\", \"type\": \"lightweight\"},\n ],\n },\n \"xai\": {\n \"display_name\": \"xAI (Grok)\",\n \"base_url\": \"https://api.x.ai/v1\",\n \"needs_proxy\": True,\n \"supports_tools\": True,\n \"api_key_url\": \"https://console.x.ai/\",\n \"models\": [\n {\"id\": \"grok-4-0709\", \"name\": \"Grok 4\", \"type\": \"chat\"},\n {\"id\": \"grok-3\", \"name\": \"Grok 3\", \"type\": \"chat\"},\n {\"id\": \"grok-3-mini\", \"name\": \"Grok 3 Mini\", \"type\": \"lightweight\"},\n ],\n },\n \"deepseek\": {\n \"display_name\": \"DeepSeek\",\n \"base_url\": \"https://api.deepseek.com\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.deepseek.com/api_keys\",\n \"models\": [\n {\"id\": \"deepseek-chat\", \"name\": \"DeepSeek-V3.2(通用)\", \"type\": \"chat\"},\n {\"id\": \"deepseek-reasoner\", \"name\": \"DeepSeek-R1(推理)\", \"type\": \"reasoning\"},\n ],\n },\n \"qwen\": {\n \"display_name\": \"通义千问 (Qwen)\",\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://bailian.console.aliyun.com/#/api-key\",\n \"models\": [\n {\"id\": \"qwen3-max\", \"name\": \"Qwen3 Max(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"qwen-max\", \"name\": \"Qwen Max\", \"type\": \"chat\"},\n {\"id\": \"qwen-plus\", \"name\": \"Qwen Plus(推荐)\", \"type\": \"chat\"},\n {\"id\": \"qwen-turbo\", \"name\": \"Qwen Turbo\", \"type\": \"lightweight\"},\n {\"id\": \"qwen-long\", \"name\": \"Qwen Long\", \"type\": \"chat\"},\n {\"id\": \"qwen3-coder-plus\", \"name\": \"Qwen3 Coder Plus(编码)\", \"type\": \"coding\"},\n {\"id\": \"qwen-coder-plus\", \"name\": \"Qwen Coder Plus\", \"type\": \"coding\"},\n {\"id\": \"qwq-plus\", \"name\": \"QwQ Plus(推理)\", \"type\": \"reasoning\"},\n {\"id\": \"qwen-flash\", \"name\": \"Qwen Flash\", \"type\": \"lightweight\"},\n ],\n },\n \"glm\": {\n \"display_name\": \"智谱 AI(通用)\",\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://open.bigmodel.cn/usercenter/apikeys\",\n \"models\": [\n {\"id\": \"glm-5\", \"name\": \"GLM-5(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"glm-4.7\", \"name\": \"GLM-4.7\", \"type\": \"chat\"},\n {\"id\": \"glm-4.7-flash\", \"name\": \"GLM-4.7 Flash(免费)\", \"type\": \"lightweight\"},\n {\"id\": \"glm-4.6\", \"name\": \"GLM-4.6\", \"type\": \"chat\"},\n {\"id\": \"glm-4.5-air\", \"name\": \"GLM-4.5 Air\", \"type\": \"chat\"},\n {\"id\": \"glm-4.5-airx\", \"name\": \"GLM-4.5 AirX\", \"type\": \"chat\"},\n {\"id\": \"glm-4.5-flash\", \"name\": \"GLM-4.5 Flash(免费)\", \"type\": \"lightweight\"},\n {\"id\": \"glm-4-flash\", \"name\": \"GLM-4 Flash(免费)\", \"type\": \"lightweight\"},\n {\"id\": \"glm-4-long\", \"name\": \"GLM-4 Long\", \"type\": \"chat\"},\n ],\n },\n \"glm-code\": {\n \"display_name\": \"智谱 AI(Coding Plan)\",\n \"base_url\": \"https://open.bigmodel.cn/api/coding/paas/v4\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://open.bigmodel.cn/usercenter/apikeys\",\n \"share_api_key_with\": \"glm\",\n \"models\": [\n {\"id\": \"GLM-5\", \"name\": \"GLM-5(旗舰)\", \"type\": \"coding\"},\n {\"id\": \"GLM-4.7\", \"name\": \"GLM-4.7\", \"type\": \"coding\"},\n {\"id\": \"GLM-4.6\", \"name\": \"GLM-4.6\", \"type\": \"coding\"},\n {\"id\": \"GLM-4.5-air\", \"name\": \"GLM-4.5 Air\", \"type\": \"coding\"},\n ],\n },\n \"minimax\": {\n \"display_name\": \"MiniMax(通用)\",\n \"base_url\": \"https://api.minimaxi.com/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.minimaxi.com/dashboard\",\n \"models\": [\n {\"id\": \"MiniMax-M2.5\", \"name\": \"MiniMax M2.5(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"MiniMax-M2.5-highspeed\", \"name\": \"MiniMax M2.5 高速版\", \"type\": \"lightweight\"},\n {\"id\": \"MiniMax-M2.1\", \"name\": \"MiniMax M2.1\", \"type\": \"chat\"},\n {\"id\": \"MiniMax-M2.1-highspeed\", \"name\": \"MiniMax M2.1 高速版\", \"type\": \"lightweight\"},\n ],\n },\n \"minimax-code\": {\n \"display_name\": \"MiniMax(Coding Plan)\",\n \"base_url\": \"https://api.minimaxi.com/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.minimaxi.com/dashboard\",\n \"share_api_key_with\": \"minimax\",\n \"models\": [\n {\"id\": \"MiniMax-M2.5\", \"name\": \"MiniMax M2.5\", \"type\": \"coding\"},\n {\"id\": \"MiniMax-M2.5-highspeed\", \"name\": \"MiniMax M2.5 高速版\", \"type\": \"coding\"},\n ],\n },\n \"kimi\": {\n \"display_name\": \"Kimi / Moonshot 开放平台\",\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.moonshot.cn/console/api-keys\",\n \"models\": [\n {\"id\": \"kimi-k2.5\", \"name\": \"Kimi K2.5(旗舰多模态)\", \"type\": \"chat\"},\n {\"id\": \"kimi-k2\", \"name\": \"Kimi K2\", \"type\": \"chat\"},\n {\"id\": \"kimi-latest\", \"name\": \"Kimi Latest\", \"type\": \"chat\"},\n {\"id\": \"moonshot-v1-auto\", \"name\": \"Moonshot v1 Auto\", \"type\": \"chat\"},\n {\"id\": \"moonshot-v1-128k\", \"name\": \"Moonshot v1 128K\", \"type\": \"chat\"},\n ],\n },\n \"kimi-code\": {\n \"display_name\": \"Kimi Code(会员编程)\",\n \"base_url\": \"https://api.kimi.com/coding/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://platform.moonshot.cn/console/api-keys\",\n \"share_api_key_with\": \"kimi\",\n \"models\": [\n {\"id\": \"kimi-for-coding\", \"name\": \"Kimi for Coding(默认)\", \"type\": \"coding\"},\n ],\n },\n \"doubao\": {\n \"display_name\": \"豆包 (Doubao)\",\n \"base_url\": \"https://ark.cn-beijing.volces.com/api/v3\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey\",\n \"models\": [\n {\"id\": \"doubao-seed-2-0-pro\", \"name\": \"Doubao Seed 2.0 Pro(旗舰)\", \"type\": \"chat\"},\n {\"id\": \"doubao-seed-2-0-code\", \"name\": \"Doubao Seed 2.0 Code(编码)\", \"type\": \"coding\"},\n {\"id\": \"doubao-seed-2-0-lite\", \"name\": \"Doubao Seed 2.0 Lite(轻量)\", \"type\": \"lightweight\"},\n ],\n },\n \"ollama\": {\n \"display_name\": \"Ollama(本地)\",\n \"base_url\": \"http://localhost:11434/v1\",\n \"needs_proxy\": False,\n \"supports_tools\": False,\n \"api_key_url\": \"https://ollama.com/download\",\n \"models\": [], # 动态检测\n },\n \"custom\": {\n \"display_name\": \"自定义(OpenAI兼容)\",\n \"base_url\": \"\",\n \"needs_proxy\": False,\n \"supports_tools\": True,\n \"api_key_url\": \"\",\n \"models\": [],\n },\n}\n\n\nclass LLMService:\n \"\"\"统一LLM服务单例\"\"\"\n\n _instance: Optional[\"LLMService\"] = None\n\n def __new__(cls) -> \"LLMService\":\n if cls._instance is None:\n cls._instance = super().__new__(cls)\n cls._instance._initialized = False\n return cls._instance\n\n def __init__(self):\n if self._initialized:\n return\n self._initialized = True\n self._provider: Optional[LLMProvider] = None\n self._active_provider_name: str = \"\"\n self._db_path: str = \"\"\n logger.info(\"LLMService 初始化完成\")\n\n def set_db_path(self, db_path: str):\n \"\"\"设置数据库路径并初始化llm_settings表\"\"\"\n self._db_path = db_path\n self._init_db()\n self._load_provider_from_db()\n\n # ============================================================\n # 数据库操作\n # ============================================================\n def _init_db(self):\n if not self._db_path:\n return\n try:\n with sqlite3.connect(self._db_path) as conn:\n conn.execute(\"\"\"\n CREATE TABLE IF NOT EXISTS llm_settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT DEFAULT (datetime('now', 'localtime'))\n )\n \"\"\")\n conn.commit()\n except Exception as e:\n logger.error(\"初始化llm_settings表失败: %s\", e)\n\n def _get_setting(self, key: str, default: str = \"\") -> str:\n if not self._db_path:\n return default\n try:\n with sqlite3.connect(self._db_path) as conn:\n row = conn.execute(\n \"SELECT value FROM llm_settings WHERE key = ?\", (key,)\n ).fetchone()\n return row[0] if row else default\n except Exception:\n return default\n\n def _set_setting(self, key: str, value: str):\n if not self._db_path:\n return\n try:\n with sqlite3.connect(self._db_path) as conn:\n conn.execute(\"\"\"\n INSERT INTO llm_settings (key, value, updated_at)\n VALUES (?, ?, datetime('now', 'localtime'))\n ON CONFLICT(key) DO UPDATE SET\n value = excluded.value,\n updated_at = excluded.updated_at\n \"\"\", (key, value))\n conn.commit()\n except Exception as e:\n logger.error(\"保存LLM设置失败 [%s]: %s\", key, e)\n\n # ============================================================\n # Provider管理\n # ============================================================\n def _load_provider_from_db(self):\n \"\"\"从数据库加载已保存的provider配置\"\"\"\n provider_name = self._get_setting(\"active_provider\", \"\")\n if not provider_name:\n return\n try:\n self._build_provider(provider_name)\n except Exception as e:\n logger.warning(\"加载已保存的LLM provider失败: %s\", e)\n\n def _build_provider(self, provider_name: str):\n \"\"\"根据provider名称构建Provider实例\"\"\"\n if provider_name not in BUILTIN_PROVIDERS:\n raise ValueError(f\"未知的LLM提供商: {provider_name}\")\n\n preset = BUILTIN_PROVIDERS[provider_name]\n\n api_key = self._get_setting(f\"{provider_name}_api_key\", \"\")\n # 支持 share_api_key_with:Coding Plan 提供商可共享主提供商的 API Key\n if not api_key and preset.get(\"share_api_key_with\"):\n api_key = self._get_setting(f\"{preset['share_api_key_with']}_api_key\", \"\")\n model = self._get_setting(f\"{provider_name}_model\", \"\")\n custom_base_url = self._get_setting(f\"{provider_name}_base_url\", \"\")\n proxy_url = self._get_setting(\"proxy_url\", \"\")\n proxy_enabled = self._get_setting(\"proxy_enabled\", \"false\") == \"true\"\n temperature = float(self._get_setting(\"temperature\", \"0.7\"))\n max_tokens = int(self._get_setting(\"max_tokens\", \"4096\"))\n\n base_url = custom_base_url or preset[\"base_url\"]\n\n # 对非Ollama提供商,没有API Key则不创建\n if provider_name not in (\"ollama\", \"custom\") and not api_key:\n logger.info(\"提供商 %s 未配置API Key,跳过\", provider_name)\n return\n\n # 默认选第一个模型\n if not model and preset[\"models\"]:\n model = preset[\"models\"][0][\"id\"]\n\n models = [\n ModelInfo(id=m[\"id\"], name=m[\"name\"], model_type=m.get(\"type\", \"chat\"))\n for m in preset[\"models\"]\n ]\n\n # 代理由用户全局开关控制\n effective_proxy = proxy_url if (proxy_enabled and proxy_url) else None\n\n config = ProviderConfig(\n provider_name=provider_name,\n display_name=preset[\"display_name\"],\n base_url=base_url,\n api_key=api_key,\n model=model,\n proxy=effective_proxy,\n temperature=temperature,\n max_tokens=max_tokens,\n models=models,\n supports_tools=preset.get(\"supports_tools\", True),\n needs_proxy=preset.get(\"needs_proxy\", False),\n api_key_url=preset.get(\"api_key_url\", \"\"),\n )\n\n self._provider = OpenAICompatibleProvider(config)\n self._active_provider_name = provider_name\n logger.info(\"LLM Provider已切换: %s / %s\", provider_name, model)\n\n def switch_provider(self, provider_name: str, config: Dict[str, Any]):\n \"\"\"切换提供商并保存配置\"\"\"\n if config.get(\"api_key\"):\n self._set_setting(f\"{provider_name}_api_key\", config[\"api_key\"])\n if config.get(\"model\"):\n self._set_setting(f\"{provider_name}_model\", config[\"model\"])\n if config.get(\"base_url\"):\n self._set_setting(f\"{provider_name}_base_url\", config[\"base_url\"])\n if config.get(\"proxy_url\") is not None:\n self._set_setting(\"proxy_url\", config[\"proxy_url\"])\n if config.get(\"proxy_enabled\") is not None:\n self._set_setting(\"proxy_enabled\", \"true\" if config[\"proxy_enabled\"] else \"false\")\n if config.get(\"temperature\") is not None:\n self._set_setting(\"temperature\", str(config[\"temperature\"]))\n if config.get(\"max_tokens\") is not None:\n self._set_setting(\"max_tokens\", str(config[\"max_tokens\"]))\n\n self._set_setting(\"active_provider\", provider_name)\n self._build_provider(provider_name)\n\n # ============================================================\n # 对外接口\n # ============================================================\n @property\n def is_configured(self) -> bool:\n return self._provider is not None\n\n @property\n def active_provider_name(self) -> str:\n return self._active_provider_name\n\n def get_provider(self) -> Optional[LLMProvider]:\n return self._provider\n\n async def chat(\n self,\n messages: List[LLMMessage],\n tools: Optional[List[LLMToolDef]] = None,\n stream: bool = False,\n temperature: Optional[float] = None,\n max_tokens: Optional[int] = None,\n ) -> LLMResponse:\n if not self._provider:\n raise RuntimeError(\"LLM服务未配置,请先在设置中选择并配置AI模型提供商\")\n return await self._provider.chat(messages, tools, stream, temperature, max_tokens)\n\n async def chat_stream(\n self,\n messages: List[LLMMessage],\n tools: Optional[List[LLMToolDef]] = None,\n temperature: Optional[float] = None,\n max_tokens: Optional[int] = None,\n ) -> AsyncIterator[str]:\n if not self._provider:\n raise RuntimeError(\"LLM服务未配置,请先在设置中选择并配置AI模型提供商\")\n async for chunk in self._provider.chat_stream(messages, tools, temperature, max_tokens):\n yield chunk\n\n async def test_connection(self) -> Dict[str, Any]:\n if not self._provider:\n return {\"success\": False, \"message\": \"LLM服务未配置\"}\n return await self._provider.test_connection()\n\n async def list_models(self, provider_name: Optional[str] = None) -> List[ModelInfo]:\n \"\"\"获取指定提供商或当前提供商的模型列表\"\"\"\n if provider_name and provider_name == \"ollama\":\n # Ollama需要动态检测\n temp_config = ProviderConfig(\n provider_name=\"ollama\",\n display_name=\"Ollama\",\n base_url=self._get_setting(\"ollama_base_url\", \"http://localhost:11434/v1\"),\n api_key=\"ollama\",\n model=\"\",\n )\n temp_provider = OpenAICompatibleProvider(temp_config)\n models = await temp_provider.list_models()\n await temp_provider.close()\n return models\n\n if provider_name and provider_name in BUILTIN_PROVIDERS:\n preset = BUILTIN_PROVIDERS[provider_name]\n return [\n ModelInfo(id=m[\"id\"], name=m[\"name\"], model_type=m.get(\"type\", \"chat\"))\n for m in preset[\"models\"]\n ]\n\n if self._provider:\n return await self._provider.list_models()\n return []\n\n def get_settings(self) -> Dict[str, Any]:\n \"\"\"获取当前所有LLM设置\"\"\"\n settings: Dict[str, Any] = {\n \"active_provider\": self._get_setting(\"active_provider\", \"\"),\n \"proxy_url\": self._get_setting(\"proxy_url\", \"\"),\n \"proxy_enabled\": self._get_setting(\"proxy_enabled\", \"false\") == \"true\",\n \"temperature\": float(self._get_setting(\"temperature\", \"0.7\")),\n \"max_tokens\": int(self._get_setting(\"max_tokens\", \"4096\")),\n }\n\n # 各提供商独立配置\n for name in BUILTIN_PROVIDERS:\n settings[f\"{name}_api_key\"] = \"***\" if self._get_setting(f\"{name}_api_key\") else \"\"\n settings[f\"{name}_model\"] = self._get_setting(f\"{name}_model\", \"\")\n settings[f\"{name}_base_url\"] = self._get_setting(f\"{name}_base_url\", \"\")\n\n return settings\n\n def get_providers_info(self) -> List[Dict[str, Any]]:\n \"\"\"获取全部可用提供商信息\"\"\"\n providers = []\n for name, preset in BUILTIN_PROVIDERS.items():\n has_key = bool(self._get_setting(f\"{name}_api_key\"))\n # Coding Plan 提供商可共享主提供商的 API Key\n share_with = preset.get(\"share_api_key_with\", \"\")\n if not has_key and share_with:\n has_key = bool(self._get_setting(f\"{share_with}_api_key\"))\n selected_model = self._get_setting(f\"{name}_model\", \"\")\n custom_base_url = self._get_setting(f\"{name}_base_url\", \"\")\n providers.append({\n \"name\": name,\n \"display_name\": preset[\"display_name\"],\n \"base_url\": preset[\"base_url\"],\n \"custom_base_url\": custom_base_url,\n \"needs_proxy\": preset.get(\"needs_proxy\", False),\n \"supports_tools\": preset.get(\"supports_tools\", True),\n \"api_key_url\": preset.get(\"api_key_url\", \"\"),\n \"has_api_key\": has_key,\n \"selected_model\": selected_model,\n \"is_active\": name == self._active_provider_name,\n \"models\": preset[\"models\"],\n \"share_api_key_with\": share_with,\n })\n return providers\n\n def save_settings(self, settings: Dict[str, Any]):\n \"\"\"批量保存设置\"\"\"\n for key, value in settings.items():\n if key.endswith(\"_api_key\") and value == \"***\":\n continue # 跳过未修改的密钥\n self._set_setting(key, str(value))\n\n # 如果active_provider变了,重建provider\n new_provider = settings.get(\"active_provider\", \"\")\n if new_provider and new_provider != self._active_provider_name:\n try:\n self._build_provider(new_provider)\n except Exception as e:\n logger.error(\"切换provider失败: %s\", e)\n\n\n# 全局单例\nllm_service = LLMService()\n"}}}}