Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/aidocplus_dragonstock.json
T

1 line
46 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}