Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/alex-conder_kaelis-archive.json
T

1 line
70 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": "alex-conder/kaelis-archive", "files": {"core/llm/smart_router.py": {"f79317192a1d3658ba12568f7c0f1ef70ba8733b": {"t": 1785745983.0382984, "n": 20300, "h": "5b62e5af109d1b40", "c": "\"\"\"\n多模型智能路由与成本控制\n\nSmartRouter + ModelRegistry\n\"\"\"\n\nimport json\nimport logging\nimport os\nimport sqlite3\nimport time\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\n\nfrom core.resilience import get_circuit_breaker\nfrom core.security.credential_vault import CredentialVault, resolve_llm_api_key\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass ModelConfig:\n \"\"\"模型配置\"\"\"\n name: str\n endpoint: str\n api_key: str\n cost_per_1m: float # 每百万 token 成本(美元)\n tags: List[str] = field(default_factory=list)\n context_length: int = 4096\n\n\nclass CostTracker:\n \"\"\"\n 成本追踪器:记录每个模型的调用次数、token 使用量与累计成本。\n 数据保存在内存中,重启清零(生产环境可持久化到 SQLite)。\n \"\"\"\n\n def __init__(self):\n self._calls: Dict[str, int] = {}\n self._tokens: Dict[str, int] = {}\n self._cost: Dict[str, float] = {}\n self._monthly_start = time.time()\n\n def record_call(self, model_name: str, tokens_used: int = 0):\n \"\"\"记录一次调用\"\"\"\n self._calls[model_name] = self._calls.get(model_name, 0) + 1\n self._tokens[model_name] = self._tokens.get(model_name, 0) + tokens_used\n cost = (tokens_used / 1_000_000) * self._get_model_cost(model_name)\n self._cost[model_name] = self._cost.get(model_name, 0.0) + cost\n\n def _get_model_cost(self, model_name: str) -> float:\n # 延迟引用外部 registry,避免循环依赖\n return 0.0\n\n def get_stats(self) -> Dict[str, Any]:\n total_calls = sum(self._calls.values())\n total_cost = sum(self._cost.values())\n return {\n \"total_calls\": total_calls,\n \"total_cost_usd\": round(total_cost, 6),\n \"monthly_start_iso\": time.strftime(\"%Y-%m-%d\", time.localtime(self._monthly_start)),\n \"by_model\": {\n name: {\n \"calls\": self._calls.get(name, 0),\n \"tokens\": self._tokens.get(name, 0),\n \"cost_usd\": round(self._cost.get(name, 0.0), 6),\n }\n for name in set(list(self._calls.keys()) + list(self._tokens.keys()) + list(self._cost.keys()))\n },\n }\n\n def reset(self):\n self._calls.clear()\n self._tokens.clear()\n self._cost.clear()\n self._monthly_start = time.time()\n\n\nclass ModelRegistry:\n \"\"\"\n 模型注册表:管理所有可用 LLM 模型。\n 支持环境变量预置 + SQLite 持久化的用户自定义模型。\n \"\"\"\n\n def __init__(self, db_path: Optional[str] = None):\n self._models: Dict[str, ModelConfig] = {}\n self._db_path = Path(db_path or os.path.expanduser(\"~/.kaelis/llm_models.db\"))\n self._db_path.parent.mkdir(parents=True, exist_ok=True)\n self._vault = CredentialVault()\n self._init_db()\n self._load_from_env()\n self._load_from_db()\n\n # ------------------------------------------------------------------ #\n # Database\n # ------------------------------------------------------------------ #\n\n def _init_db(self):\n with sqlite3.connect(str(self._db_path)) as conn:\n conn.execute(\"\"\"\n CREATE TABLE IF NOT EXISTS llm_user_models (\n name TEXT PRIMARY KEY,\n endpoint TEXT NOT NULL,\n cost_per_1m REAL NOT NULL,\n tags TEXT,\n context_length INTEGER DEFAULT 4096,\n created_at TEXT\n )\n \"\"\")\n conn.commit()\n\n def _save_to_db(self, name: str, endpoint: str, cost_per_1m: float,\n tags: List[str], context_length: int):\n with sqlite3.connect(str(self._db_path)) as conn:\n conn.execute(\"\"\"\n INSERT OR REPLACE INTO llm_user_models\n (name, endpoint, cost_per_1m, tags, context_length, created_at)\n VALUES (?, ?, ?, ?, ?, ?)\n \"\"\", (name, endpoint, cost_per_1m, json.dumps(tags), context_length,\n time.strftime(\"%Y-%m-%d %H:%M:%S\")))\n conn.commit()\n\n def _delete_from_db(self, name: str):\n with sqlite3.connect(str(self._db_path)) as conn:\n conn.execute(\"DELETE FROM llm_user_models WHERE name = ?\", (name,))\n conn.commit()\n\n def _load_from_db(self):\n try:\n with sqlite3.connect(str(self._db_path)) as conn:\n rows = conn.execute(\"SELECT * FROM llm_user_models\").fetchall()\n for row in rows:\n name, endpoint, cost_per_1m, tags_raw, context_length, _ = row\n api_key = self._vault.get(f\"model:{name}:api_key\") or \"\"\n if api_key:\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=json.loads(tags_raw) if tags_raw else [],\n context_length=context_length or 4096,\n )\n logger.info(\"[ModelRegistry] Loaded from DB: %s\", name)\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to load from DB: %s\", e)\n\n # ------------------------------------------------------------------ #\n # Public API\n # ------------------------------------------------------------------ #\n\n def add_model(\n self,\n name: str,\n endpoint: str,\n api_key: str,\n cost_per_1m: float,\n tags: Optional[List[str]] = None,\n context_length: int = 4096,\n ) -> bool:\n \"\"\"注册新模型(自动持久化到 SQLite + Vault)\"\"\"\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=tags or [],\n context_length=context_length,\n )\n # 持久化:配置写入 SQLite,API Key 写入 Vault\n try:\n self._save_to_db(name, endpoint, cost_per_1m, tags or [], context_length)\n if api_key:\n self._vault.set(f\"model:{name}:api_key\", api_key)\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to persist model %s: %s\", name, e)\n logger.info(\"[ModelRegistry] Registered: %s ($%s/1M)\", name, cost_per_1m)\n return True\n\n def remove_model(self, name: str) -> bool:\n if name in self._models:\n del self._models[name]\n try:\n self._delete_from_db(name)\n self._vault.delete(f\"model:{name}:api_key\")\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to remove persisted model %s: %s\", name, e)\n return True\n return False\n\n def update_model(\n self,\n name: str,\n endpoint: str,\n api_key: str,\n cost_per_1m: float,\n tags: Optional[List[str]] = None,\n context_length: int = 4096,\n ) -> bool:\n \"\"\"更新已有模型配置(内存 + SQLite + Vault)\"\"\"\n if name not in self._models:\n return False\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=tags or [],\n context_length=context_length,\n )\n try:\n self._save_to_db(name, endpoint, cost_per_1m, tags or [], context_length)\n if api_key:\n self._vault.set(f\"model:{name}:api_key\", api_key)\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to persist updated model %s: %s\", name, e)\n logger.info(\"[ModelRegistry] Updated: %s\", name)\n return True\n\n def test_model_connection(self, name: str) -> Dict[str, Any]:\n \"\"\"探测模型端点连通性,返回 {success, latency_ms, error}。不记录 API Key。\"\"\"\n import requests\n\n model = self._models.get(name)\n if not model:\n return {\"success\": False, \"error\": \"Model not found\"}\n\n start = time.time()\n try:\n headers = {\"Authorization\": f\"Bearer {model.api_key}\"}\n test_url = model.endpoint.rstrip(\"/\") + \"/models\"\n resp = requests.get(test_url, headers=headers, timeout=10, allow_redirects=True)\n latency_ms = int((time.time() - start) * 1000)\n if resp.status_code == 200:\n return {\"success\": True, \"latency_ms\": latency_ms}\n if resp.status_code in (401, 403):\n return {\"success\": False, \"error\": \"Authentication error\"}\n return {\"success\": False, \"error\": f\"HTTP {resp.status_code}\"}\n except requests.exceptions.ConnectionError:\n return {\"success\": False, \"error\": \"Connection error\"}\n except requests.exceptions.Timeout:\n return {\"success\": False, \"error\": \"Timeout\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n def get_model(self, name: str) -> Optional[ModelConfig]:\n return self._models.get(name)\n\n def get_models(self) -> List[Dict]:\n \"\"\"返回所有模型配置(字典形式,不含 api_key)\"\"\"\n return [\n {\n \"name\": m.name,\n \"endpoint\": m.endpoint,\n \"cost_per_1m\": m.cost_per_1m,\n \"tags\": m.tags,\n \"context_length\": m.context_length,\n }\n for m in self._models.values()\n ]\n\n def _load_from_env(self):\n \"\"\"从环境变量和 CredentialVault 自动加载预置模型\"\"\"\n # OpenAI\n openai_key = resolve_llm_api_key(\"openai\")\n if openai_key:\n self.add_model(\n name=\"gpt-4o\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=5.0,\n tags=[\"strong\", \"code\", \"analysis\", \"conversation\", \"safety\"],\n context_length=128000,\n )\n self.add_model(\n name=\"gpt-4o-mini\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=0.15,\n tags=[\"cheap\", \"summary\", \"conversation\"],\n context_length=128000,\n )\n\n # DeepSeek\n deepseek_key = resolve_llm_api_key(\"deepseek\")\n if deepseek_key:\n self.add_model(\n name=\"deepseek-chat\",\n endpoint=\"https://api.deepseek.com/v1\",\n api_key=deepseek_key,\n cost_per_1m=0.14,\n tags=[\"cheap\", \"code\", \"analysis\", \"conversation\"],\n context_length=64000,\n )\n\n # Claude\n claude_key = resolve_llm_api_key(\"anthropic\")\n if claude_key:\n self.add_model(\n name=\"claude-3-5-sonnet\",\n endpoint=\"https://api.anthropic.com/v1\",\n api_key=claude_key,\n cost_per_1m=3.0,\n tags=[\"strong\", \"code\", \"analysis\", \"safety\"],\n context_length=200000,\n )\n\n\nclass SmartRouter:\n \"\"\"\n 多模型智能路由。\n\n 路由策略:\n 1. 任务分类(基于关键词启发式 + 可选 LLM 分类)\n 2. 模型匹配(tags 匹配)\n 3. 成本排序(cost_per_1m 升序)\n 4. 熔断检查(跳过故障模型)\n 5. 预算约束(排除超预算模型)\n 6. 选择最优模型\n \"\"\"\n\n # 任务分类关键词映射\n TASK_CLASSIFICATION = {\n \"code\": [\"代码\", \"code\", \"编程\", \"programming\", \"算法\", \"algorithm\", \"debug\", \"函数\", \"class\", \"optimize\", \"优化\", \"query\", \"sql\", \"database\"],\n \"analysis\": [\"分析\", \"analysis\", \"评估\", \"evaluate\", \"比较\", \"compare\", \"研究\", \"research\"],\n \"summary\": [\"总结\", \"summary\", \"摘要\", \"abstract\", \"概括\", \"overview\", \"提炼\"],\n \"conversation\": [\"对话\", \"conversation\", \"聊天\", \"chat\", \"问答\", \"q&a\", \"交流\"],\n \"safety\": [\"安全\", \"safety\", \"审核\", \"audit\", \"风险\", \"risk\", \"合规\", \"compliance\"],\n }\n\n def __init__(self, registry: Optional[ModelRegistry] = None):\n self.registry = registry or ModelRegistry()\n self.tracker = CostTracker()\n self._strategy: str = \"balanced\"\n\n @property\n def strategy(self) -> str:\n return self._strategy\n\n @strategy.setter\n def strategy(self, value: str):\n if value not in (\"cost_first\", \"quality_first\", \"balanced\"):\n raise ValueError(f\"Unknown strategy: {value}\")\n self._strategy = value\n\n def _classify_task(self, task_description: str) -> List[str]:\n \"\"\"基于关键词对任务进行分类\"\"\"\n task_lower = task_description.lower()\n categories = []\n for category, keywords in self.TASK_CLASSIFICATION.items():\n if any(kw.lower() in task_lower for kw in keywords):\n categories.append(category)\n if not categories:\n categories.append(\"conversation\") # 默认分类\n return categories\n\n def _match_models(self, categories: List[str], context_length_required: int = 0) -> List[ModelConfig]:\n \"\"\"根据分类匹配候选模型\"\"\"\n candidates = []\n for model in self.registry._models.values():\n # 上下文长度检查\n if context_length_required > 0 and model.context_length < context_length_required:\n continue\n # Tags 匹配(至少匹配一个分类)\n if any(cat in model.tags for cat in categories):\n candidates.append(model)\n return candidates\n\n def _filter_available(\n self,\n candidates: List[ModelConfig],\n max_cost_budget: Optional[float] = None,\n ) -> List[ModelConfig]:\n \"\"\"应用预算和熔断过滤\"\"\"\n available = []\n for model in candidates:\n if max_cost_budget is not None and model.cost_per_1m > max_cost_budget:\n continue\n cb = get_circuit_breaker(f\"llm:{model.name}\")\n if cb.is_open():\n logger.warning(f\"[SmartRouter] Model {model.name} circuit OPEN, skipping\")\n continue\n available.append(model)\n return available\n\n def _sort_by_strategy(self, models: List[ModelConfig], strategy: str) -> List[ModelConfig]:\n \"\"\"根据策略排序模型\"\"\"\n if strategy == \"cost_first\":\n models.sort(key=lambda m: m.cost_per_1m)\n elif strategy == \"quality_first\":\n models.sort(key=lambda m: (-m.context_length, m.cost_per_1m))\n else: # balanced\n models.sort(key=lambda m: m.context_length / (m.cost_per_1m + 0.01), reverse=True)\n return models\n\n def _estimate_cost(self, model: ModelConfig, context_length_required: int = 0) -> float:\n \"\"\"估算单次调用成本(假设 1K 输出 tokens)\"\"\"\n input_tokens = max(context_length_required, 1000)\n output_tokens = 1000\n total = input_tokens + output_tokens\n return round((total / 1_000_000) * model.cost_per_1m, 8)\n\n def route(\n self,\n task_description: str,\n context_length_required: int = 0,\n max_cost_budget: Optional[float] = None,\n strategy: str = \"balanced\",\n ) -> Optional[Dict]:\n \"\"\"\n 同步路由到最优模型。\n 返回模型配置字典 + 预估成本,或 None(无可用模型)。\n \"\"\"\n categories = self._classify_task(task_description)\n candidates = self._match_models(categories, context_length_required)\n\n if not candidates:\n candidates = list(self.registry._models.values())\n\n available = self._filter_available(candidates, max_cost_budget)\n\n if not available:\n return None\n\n available = self._sort_by_strategy(available, strategy)\n winner = available[0]\n\n estimated_cost = self._estimate_cost(winner, context_length_required)\n\n return {\n \"name\": winner.name,\n \"endpoint\": winner.endpoint,\n \"api_key\": winner.api_key,\n \"cost_per_1m\": winner.cost_per_1m,\n \"tags\": winner.tags,\n \"context_length\": winner.context_length,\n \"matched_categories\": categories,\n \"strategy\": strategy,\n \"estimated_cost\": estimated_cost,\n }\n\n async def aroute(\n self,\n task_description: str,\n context_length_required: int = 0,\n budget_limit: Optional[float] = None,\n strategy: str = \"balanced\",\n ) -> Optional[Dict]:\n \"\"\"\n 异步路由到最优模型。\n 与 sync route 行为一致,预留 LLM-based classification 的异步扩展点。\n \"\"\"\n # TODO: 未来可在此接入异步 LLM 分类器\n result = self.route(\n task_description=task_description,\n context_length_required=context_length_required,\n max_cost_budget=budget_limit,\n strategy=strategy,\n )\n return result\n\n def record_failure(self, model_name: str):\n \"\"\"记录模型调用失败(供熔断器使用)\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_failure()\n\n def record_success(self, model_name: str):\n \"\"\"记录模型调用成功\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_success()\n\n def get_stats(self) -> Dict[str, Any]:\n \"\"\"返回路由统计信息\"\"\"\n return self.tracker.get_stats()\n\n def reset_stats(self):\n \"\"\"重置统计\"\"\"\n self.tracker.reset()\n\n def get_circuit_status(self) -> Dict[str, Any]:\n \"\"\"返回所有模型的熔断器状态\"\"\"\n status = {}\n for name in self.registry._models:\n cb = get_circuit_breaker(f\"llm:{name}\")\n status[name] = {\n \"state\": cb.state.value,\n \"failure_count\": cb._failure_count,\n \"is_open\": cb.is_open(),\n }\n return status\n\n\n# =============================================================================\n# MCP Tool Registration\n# =============================================================================\n\ndef register_llm_routing_tools(mcp: Any):\n \"\"\"向 FastMCP 实例注册 LLM 路由相关 Tools。\"\"\"\n from core.llm.smart_router import ModelRegistry, SmartRouter\n\n _llm_registry = ModelRegistry()\n _smart_router = SmartRouter(_llm_registry)\n\n @mcp.tool(\"llm.route_task\")\n def llm_route_task(\n task_description: str,\n context_length: int = 0,\n budget_limit: float = 0.0,\n strategy: str = \"balanced\",\n ) -> str:\n \"\"\"\n 智能路由:根据任务描述推荐最优 LLM 模型,并返回预估成本。\n\n Args:\n task_description: 任务描述(如\"总结这篇文章\"、\"优化复杂数据库查询\")\n context_length: 需要的上下文长度(默认 0 表示不限制)\n budget_limit: 预算上限($/1M tokens),0 表示无限制\n strategy: 路由策略:cost_first | quality_first | balanced\n\n Returns:\n JSON 字符串,包含 recommended_model、estimated_cost、matched_categories\n \"\"\"\n try:\n budget = budget_limit if budget_limit > 0 else None\n result = _smart_router.route(\n task_description=task_description,\n context_length_required=context_length,\n max_cost_budget=budget,\n strategy=strategy,\n )\n if result:\n return json.dumps({\n \"success\": True,\n \"recommended_model\": result[\"name\"],\n \"endpoint\": result[\"endpoint\"],\n \"estimated_cost_usd\": result[\"estimated_cost\"],\n \"cost_per_1m\": result[\"cost_per_1m\"],\n \"context_length\": result[\"context_length\"],\n \"matched_categories\": result[\"matched_categories\"],\n \"strategy\": result[\"strategy\"],\n }, ensure_ascii=False)\n return json.dumps({\"success\": False, \"error\": \"No available model\"}, ensure_ascii=False)\n except Exception as e:\n logger.exception(\"llm.route_task failed\")\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n @mcp.tool(\"llm.list_models\")\n def llm_list_models() -> str:\n \"\"\"列出所有已注册模型。\"\"\"\n try:\n models = _llm_registry.get_models()\n return json.dumps({\"success\": True, \"models\": models}, ensure_ascii=False)\n except Exception as e:\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n @mcp.tool(\"llm.get_stats\")\n def llm_get_stats() -> str:\n \"\"\"获取 LLM 路由调用统计。\"\"\"\n try:\n stats = _smart_router.get_stats()\n return json.dumps({\"success\": True, \"stats\": stats}, ensure_ascii=False)\n except Exception as e:\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n logger.info(\"LLM routing MCP tools registered: llm.route_task, llm.list_models, llm.get_stats\")\n"}, "b0ae8e6142a923248b077cb311599fb70fc46161": {"t": 1785758507.6091135, "n": 20300, "h": "5b62e5af109d1b40", "c": "\"\"\"\n多模型智能路由与成本控制\n\nSmartRouter + ModelRegistry\n\"\"\"\n\nimport json\nimport logging\nimport os\nimport sqlite3\nimport time\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\n\nfrom core.resilience import get_circuit_breaker\nfrom core.security.credential_vault import CredentialVault, resolve_llm_api_key\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass ModelConfig:\n \"\"\"模型配置\"\"\"\n name: str\n endpoint: str\n api_key: str\n cost_per_1m: float # 每百万 token 成本(美元)\n tags: List[str] = field(default_factory=list)\n context_length: int = 4096\n\n\nclass CostTracker:\n \"\"\"\n 成本追踪器:记录每个模型的调用次数、token 使用量与累计成本。\n 数据保存在内存中,重启清零(生产环境可持久化到 SQLite)。\n \"\"\"\n\n def __init__(self):\n self._calls: Dict[str, int] = {}\n self._tokens: Dict[str, int] = {}\n self._cost: Dict[str, float] = {}\n self._monthly_start = time.time()\n\n def record_call(self, model_name: str, tokens_used: int = 0):\n \"\"\"记录一次调用\"\"\"\n self._calls[model_name] = self._calls.get(model_name, 0) + 1\n self._tokens[model_name] = self._tokens.get(model_name, 0) + tokens_used\n cost = (tokens_used / 1_000_000) * self._get_model_cost(model_name)\n self._cost[model_name] = self._cost.get(model_name, 0.0) + cost\n\n def _get_model_cost(self, model_name: str) -> float:\n # 延迟引用外部 registry,避免循环依赖\n return 0.0\n\n def get_stats(self) -> Dict[str, Any]:\n total_calls = sum(self._calls.values())\n total_cost = sum(self._cost.values())\n return {\n \"total_calls\": total_calls,\n \"total_cost_usd\": round(total_cost, 6),\n \"monthly_start_iso\": time.strftime(\"%Y-%m-%d\", time.localtime(self._monthly_start)),\n \"by_model\": {\n name: {\n \"calls\": self._calls.get(name, 0),\n \"tokens\": self._tokens.get(name, 0),\n \"cost_usd\": round(self._cost.get(name, 0.0), 6),\n }\n for name in set(list(self._calls.keys()) + list(self._tokens.keys()) + list(self._cost.keys()))\n },\n }\n\n def reset(self):\n self._calls.clear()\n self._tokens.clear()\n self._cost.clear()\n self._monthly_start = time.time()\n\n\nclass ModelRegistry:\n \"\"\"\n 模型注册表:管理所有可用 LLM 模型。\n 支持环境变量预置 + SQLite 持久化的用户自定义模型。\n \"\"\"\n\n def __init__(self, db_path: Optional[str] = None):\n self._models: Dict[str, ModelConfig] = {}\n self._db_path = Path(db_path or os.path.expanduser(\"~/.kaelis/llm_models.db\"))\n self._db_path.parent.mkdir(parents=True, exist_ok=True)\n self._vault = CredentialVault()\n self._init_db()\n self._load_from_env()\n self._load_from_db()\n\n # ------------------------------------------------------------------ #\n # Database\n # ------------------------------------------------------------------ #\n\n def _init_db(self):\n with sqlite3.connect(str(self._db_path)) as conn:\n conn.execute(\"\"\"\n CREATE TABLE IF NOT EXISTS llm_user_models (\n name TEXT PRIMARY KEY,\n endpoint TEXT NOT NULL,\n cost_per_1m REAL NOT NULL,\n tags TEXT,\n context_length INTEGER DEFAULT 4096,\n created_at TEXT\n )\n \"\"\")\n conn.commit()\n\n def _save_to_db(self, name: str, endpoint: str, cost_per_1m: float,\n tags: List[str], context_length: int):\n with sqlite3.connect(str(self._db_path)) as conn:\n conn.execute(\"\"\"\n INSERT OR REPLACE INTO llm_user_models\n (name, endpoint, cost_per_1m, tags, context_length, created_at)\n VALUES (?, ?, ?, ?, ?, ?)\n \"\"\", (name, endpoint, cost_per_1m, json.dumps(tags), context_length,\n time.strftime(\"%Y-%m-%d %H:%M:%S\")))\n conn.commit()\n\n def _delete_from_db(self, name: str):\n with sqlite3.connect(str(self._db_path)) as conn:\n conn.execute(\"DELETE FROM llm_user_models WHERE name = ?\", (name,))\n conn.commit()\n\n def _load_from_db(self):\n try:\n with sqlite3.connect(str(self._db_path)) as conn:\n rows = conn.execute(\"SELECT * FROM llm_user_models\").fetchall()\n for row in rows:\n name, endpoint, cost_per_1m, tags_raw, context_length, _ = row\n api_key = self._vault.get(f\"model:{name}:api_key\") or \"\"\n if api_key:\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=json.loads(tags_raw) if tags_raw else [],\n context_length=context_length or 4096,\n )\n logger.info(\"[ModelRegistry] Loaded from DB: %s\", name)\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to load from DB: %s\", e)\n\n # ------------------------------------------------------------------ #\n # Public API\n # ------------------------------------------------------------------ #\n\n def add_model(\n self,\n name: str,\n endpoint: str,\n api_key: str,\n cost_per_1m: float,\n tags: Optional[List[str]] = None,\n context_length: int = 4096,\n ) -> bool:\n \"\"\"注册新模型(自动持久化到 SQLite + Vault)\"\"\"\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=tags or [],\n context_length=context_length,\n )\n # 持久化:配置写入 SQLite,API Key 写入 Vault\n try:\n self._save_to_db(name, endpoint, cost_per_1m, tags or [], context_length)\n if api_key:\n self._vault.set(f\"model:{name}:api_key\", api_key)\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to persist model %s: %s\", name, e)\n logger.info(\"[ModelRegistry] Registered: %s ($%s/1M)\", name, cost_per_1m)\n return True\n\n def remove_model(self, name: str) -> bool:\n if name in self._models:\n del self._models[name]\n try:\n self._delete_from_db(name)\n self._vault.delete(f\"model:{name}:api_key\")\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to remove persisted model %s: %s\", name, e)\n return True\n return False\n\n def update_model(\n self,\n name: str,\n endpoint: str,\n api_key: str,\n cost_per_1m: float,\n tags: Optional[List[str]] = None,\n context_length: int = 4096,\n ) -> bool:\n \"\"\"更新已有模型配置(内存 + SQLite + Vault)\"\"\"\n if name not in self._models:\n return False\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=tags or [],\n context_length=context_length,\n )\n try:\n self._save_to_db(name, endpoint, cost_per_1m, tags or [], context_length)\n if api_key:\n self._vault.set(f\"model:{name}:api_key\", api_key)\n except Exception as e:\n logger.warning(\"[ModelRegistry] Failed to persist updated model %s: %s\", name, e)\n logger.info(\"[ModelRegistry] Updated: %s\", name)\n return True\n\n def test_model_connection(self, name: str) -> Dict[str, Any]:\n \"\"\"探测模型端点连通性,返回 {success, latency_ms, error}。不记录 API Key。\"\"\"\n import requests\n\n model = self._models.get(name)\n if not model:\n return {\"success\": False, \"error\": \"Model not found\"}\n\n start = time.time()\n try:\n headers = {\"Authorization\": f\"Bearer {model.api_key}\"}\n test_url = model.endpoint.rstrip(\"/\") + \"/models\"\n resp = requests.get(test_url, headers=headers, timeout=10, allow_redirects=True)\n latency_ms = int((time.time() - start) * 1000)\n if resp.status_code == 200:\n return {\"success\": True, \"latency_ms\": latency_ms}\n if resp.status_code in (401, 403):\n return {\"success\": False, \"error\": \"Authentication error\"}\n return {\"success\": False, \"error\": f\"HTTP {resp.status_code}\"}\n except requests.exceptions.ConnectionError:\n return {\"success\": False, \"error\": \"Connection error\"}\n except requests.exceptions.Timeout:\n return {\"success\": False, \"error\": \"Timeout\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n def get_model(self, name: str) -> Optional[ModelConfig]:\n return self._models.get(name)\n\n def get_models(self) -> List[Dict]:\n \"\"\"返回所有模型配置(字典形式,不含 api_key)\"\"\"\n return [\n {\n \"name\": m.name,\n \"endpoint\": m.endpoint,\n \"cost_per_1m\": m.cost_per_1m,\n \"tags\": m.tags,\n \"context_length\": m.context_length,\n }\n for m in self._models.values()\n ]\n\n def _load_from_env(self):\n \"\"\"从环境变量和 CredentialVault 自动加载预置模型\"\"\"\n # OpenAI\n openai_key = resolve_llm_api_key(\"openai\")\n if openai_key:\n self.add_model(\n name=\"gpt-4o\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=5.0,\n tags=[\"strong\", \"code\", \"analysis\", \"conversation\", \"safety\"],\n context_length=128000,\n )\n self.add_model(\n name=\"gpt-4o-mini\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=0.15,\n tags=[\"cheap\", \"summary\", \"conversation\"],\n context_length=128000,\n )\n\n # DeepSeek\n deepseek_key = resolve_llm_api_key(\"deepseek\")\n if deepseek_key:\n self.add_model(\n name=\"deepseek-chat\",\n endpoint=\"https://api.deepseek.com/v1\",\n api_key=deepseek_key,\n cost_per_1m=0.14,\n tags=[\"cheap\", \"code\", \"analysis\", \"conversation\"],\n context_length=64000,\n )\n\n # Claude\n claude_key = resolve_llm_api_key(\"anthropic\")\n if claude_key:\n self.add_model(\n name=\"claude-3-5-sonnet\",\n endpoint=\"https://api.anthropic.com/v1\",\n api_key=claude_key,\n cost_per_1m=3.0,\n tags=[\"strong\", \"code\", \"analysis\", \"safety\"],\n context_length=200000,\n )\n\n\nclass SmartRouter:\n \"\"\"\n 多模型智能路由。\n\n 路由策略:\n 1. 任务分类(基于关键词启发式 + 可选 LLM 分类)\n 2. 模型匹配(tags 匹配)\n 3. 成本排序(cost_per_1m 升序)\n 4. 熔断检查(跳过故障模型)\n 5. 预算约束(排除超预算模型)\n 6. 选择最优模型\n \"\"\"\n\n # 任务分类关键词映射\n TASK_CLASSIFICATION = {\n \"code\": [\"代码\", \"code\", \"编程\", \"programming\", \"算法\", \"algorithm\", \"debug\", \"函数\", \"class\", \"optimize\", \"优化\", \"query\", \"sql\", \"database\"],\n \"analysis\": [\"分析\", \"analysis\", \"评估\", \"evaluate\", \"比较\", \"compare\", \"研究\", \"research\"],\n \"summary\": [\"总结\", \"summary\", \"摘要\", \"abstract\", \"概括\", \"overview\", \"提炼\"],\n \"conversation\": [\"对话\", \"conversation\", \"聊天\", \"chat\", \"问答\", \"q&a\", \"交流\"],\n \"safety\": [\"安全\", \"safety\", \"审核\", \"audit\", \"风险\", \"risk\", \"合规\", \"compliance\"],\n }\n\n def __init__(self, registry: Optional[ModelRegistry] = None):\n self.registry = registry or ModelRegistry()\n self.tracker = CostTracker()\n self._strategy: str = \"balanced\"\n\n @property\n def strategy(self) -> str:\n return self._strategy\n\n @strategy.setter\n def strategy(self, value: str):\n if value not in (\"cost_first\", \"quality_first\", \"balanced\"):\n raise ValueError(f\"Unknown strategy: {value}\")\n self._strategy = value\n\n def _classify_task(self, task_description: str) -> List[str]:\n \"\"\"基于关键词对任务进行分类\"\"\"\n task_lower = task_description.lower()\n categories = []\n for category, keywords in self.TASK_CLASSIFICATION.items():\n if any(kw.lower() in task_lower for kw in keywords):\n categories.append(category)\n if not categories:\n categories.append(\"conversation\") # 默认分类\n return categories\n\n def _match_models(self, categories: List[str], context_length_required: int = 0) -> List[ModelConfig]:\n \"\"\"根据分类匹配候选模型\"\"\"\n candidates = []\n for model in self.registry._models.values():\n # 上下文长度检查\n if context_length_required > 0 and model.context_length < context_length_required:\n continue\n # Tags 匹配(至少匹配一个分类)\n if any(cat in model.tags for cat in categories):\n candidates.append(model)\n return candidates\n\n def _filter_available(\n self,\n candidates: List[ModelConfig],\n max_cost_budget: Optional[float] = None,\n ) -> List[ModelConfig]:\n \"\"\"应用预算和熔断过滤\"\"\"\n available = []\n for model in candidates:\n if max_cost_budget is not None and model.cost_per_1m > max_cost_budget:\n continue\n cb = get_circuit_breaker(f\"llm:{model.name}\")\n if cb.is_open():\n logger.warning(f\"[SmartRouter] Model {model.name} circuit OPEN, skipping\")\n continue\n available.append(model)\n return available\n\n def _sort_by_strategy(self, models: List[ModelConfig], strategy: str) -> List[ModelConfig]:\n \"\"\"根据策略排序模型\"\"\"\n if strategy == \"cost_first\":\n models.sort(key=lambda m: m.cost_per_1m)\n elif strategy == \"quality_first\":\n models.sort(key=lambda m: (-m.context_length, m.cost_per_1m))\n else: # balanced\n models.sort(key=lambda m: m.context_length / (m.cost_per_1m + 0.01), reverse=True)\n return models\n\n def _estimate_cost(self, model: ModelConfig, context_length_required: int = 0) -> float:\n \"\"\"估算单次调用成本(假设 1K 输出 tokens)\"\"\"\n input_tokens = max(context_length_required, 1000)\n output_tokens = 1000\n total = input_tokens + output_tokens\n return round((total / 1_000_000) * model.cost_per_1m, 8)\n\n def route(\n self,\n task_description: str,\n context_length_required: int = 0,\n max_cost_budget: Optional[float] = None,\n strategy: str = \"balanced\",\n ) -> Optional[Dict]:\n \"\"\"\n 同步路由到最优模型。\n 返回模型配置字典 + 预估成本,或 None(无可用模型)。\n \"\"\"\n categories = self._classify_task(task_description)\n candidates = self._match_models(categories, context_length_required)\n\n if not candidates:\n candidates = list(self.registry._models.values())\n\n available = self._filter_available(candidates, max_cost_budget)\n\n if not available:\n return None\n\n available = self._sort_by_strategy(available, strategy)\n winner = available[0]\n\n estimated_cost = self._estimate_cost(winner, context_length_required)\n\n return {\n \"name\": winner.name,\n \"endpoint\": winner.endpoint,\n \"api_key\": winner.api_key,\n \"cost_per_1m\": winner.cost_per_1m,\n \"tags\": winner.tags,\n \"context_length\": winner.context_length,\n \"matched_categories\": categories,\n \"strategy\": strategy,\n \"estimated_cost\": estimated_cost,\n }\n\n async def aroute(\n self,\n task_description: str,\n context_length_required: int = 0,\n budget_limit: Optional[float] = None,\n strategy: str = \"balanced\",\n ) -> Optional[Dict]:\n \"\"\"\n 异步路由到最优模型。\n 与 sync route 行为一致,预留 LLM-based classification 的异步扩展点。\n \"\"\"\n # TODO: 未来可在此接入异步 LLM 分类器\n result = self.route(\n task_description=task_description,\n context_length_required=context_length_required,\n max_cost_budget=budget_limit,\n strategy=strategy,\n )\n return result\n\n def record_failure(self, model_name: str):\n \"\"\"记录模型调用失败(供熔断器使用)\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_failure()\n\n def record_success(self, model_name: str):\n \"\"\"记录模型调用成功\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_success()\n\n def get_stats(self) -> Dict[str, Any]:\n \"\"\"返回路由统计信息\"\"\"\n return self.tracker.get_stats()\n\n def reset_stats(self):\n \"\"\"重置统计\"\"\"\n self.tracker.reset()\n\n def get_circuit_status(self) -> Dict[str, Any]:\n \"\"\"返回所有模型的熔断器状态\"\"\"\n status = {}\n for name in self.registry._models:\n cb = get_circuit_breaker(f\"llm:{name}\")\n status[name] = {\n \"state\": cb.state.value,\n \"failure_count\": cb._failure_count,\n \"is_open\": cb.is_open(),\n }\n return status\n\n\n# =============================================================================\n# MCP Tool Registration\n# =============================================================================\n\ndef register_llm_routing_tools(mcp: Any):\n \"\"\"向 FastMCP 实例注册 LLM 路由相关 Tools。\"\"\"\n from core.llm.smart_router import ModelRegistry, SmartRouter\n\n _llm_registry = ModelRegistry()\n _smart_router = SmartRouter(_llm_registry)\n\n @mcp.tool(\"llm.route_task\")\n def llm_route_task(\n task_description: str,\n context_length: int = 0,\n budget_limit: float = 0.0,\n strategy: str = \"balanced\",\n ) -> str:\n \"\"\"\n 智能路由:根据任务描述推荐最优 LLM 模型,并返回预估成本。\n\n Args:\n task_description: 任务描述(如\"总结这篇文章\"、\"优化复杂数据库查询\")\n context_length: 需要的上下文长度(默认 0 表示不限制)\n budget_limit: 预算上限($/1M tokens),0 表示无限制\n strategy: 路由策略:cost_first | quality_first | balanced\n\n Returns:\n JSON 字符串,包含 recommended_model、estimated_cost、matched_categories\n \"\"\"\n try:\n budget = budget_limit if budget_limit > 0 else None\n result = _smart_router.route(\n task_description=task_description,\n context_length_required=context_length,\n max_cost_budget=budget,\n strategy=strategy,\n )\n if result:\n return json.dumps({\n \"success\": True,\n \"recommended_model\": result[\"name\"],\n \"endpoint\": result[\"endpoint\"],\n \"estimated_cost_usd\": result[\"estimated_cost\"],\n \"cost_per_1m\": result[\"cost_per_1m\"],\n \"context_length\": result[\"context_length\"],\n \"matched_categories\": result[\"matched_categories\"],\n \"strategy\": result[\"strategy\"],\n }, ensure_ascii=False)\n return json.dumps({\"success\": False, \"error\": \"No available model\"}, ensure_ascii=False)\n except Exception as e:\n logger.exception(\"llm.route_task failed\")\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n @mcp.tool(\"llm.list_models\")\n def llm_list_models() -> str:\n \"\"\"列出所有已注册模型。\"\"\"\n try:\n models = _llm_registry.get_models()\n return json.dumps({\"success\": True, \"models\": models}, ensure_ascii=False)\n except Exception as e:\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n @mcp.tool(\"llm.get_stats\")\n def llm_get_stats() -> str:\n \"\"\"获取 LLM 路由调用统计。\"\"\"\n try:\n stats = _smart_router.get_stats()\n return json.dumps({\"success\": True, \"stats\": stats}, ensure_ascii=False)\n except Exception as e:\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n logger.info(\"LLM routing MCP tools registered: llm.route_task, llm.list_models, llm.get_stats\")\n"}, "93b689a6c54d5bd4c4c29012791747fc04747b07": {"t": 1785758509.399772, "n": 14667, "h": "991977864cc59d2a", "c": "\"\"\"\n多模型智能路由与成本控制\n\nSmartRouter + ModelRegistry\n\"\"\"\n\nimport json\nimport logging\nimport os\nimport time\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, List, Optional\n\nfrom core.resilience import get_circuit_breaker\nfrom core.security.credential_vault import CredentialVault\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass ModelConfig:\n \"\"\"模型配置\"\"\"\n name: str\n endpoint: str\n api_key: str\n cost_per_1m: float # 每百万 token 成本(美元)\n tags: List[str] = field(default_factory=list)\n context_length: int = 4096\n\n\nclass CostTracker:\n \"\"\"\n 成本追踪器:记录每个模型的调用次数、token 使用量与累计成本。\n 数据保存在内存中,重启清零(生产环境可持久化到 SQLite)。\n \"\"\"\n\n def __init__(self):\n self._calls: Dict[str, int] = {}\n self._tokens: Dict[str, int] = {}\n self._cost: Dict[str, float] = {}\n self._monthly_start = time.time()\n\n def record_call(self, model_name: str, tokens_used: int = 0):\n \"\"\"记录一次调用\"\"\"\n self._calls[model_name] = self._calls.get(model_name, 0) + 1\n self._tokens[model_name] = self._tokens.get(model_name, 0) + tokens_used\n cost = (tokens_used / 1_000_000) * self._get_model_cost(model_name)\n self._cost[model_name] = self._cost.get(model_name, 0.0) + cost\n\n def _get_model_cost(self, model_name: str) -> float:\n # 延迟引用外部 registry,避免循环依赖\n return 0.0\n\n def get_stats(self) -> Dict[str, Any]:\n total_calls = sum(self._calls.values())\n total_cost = sum(self._cost.values())\n return {\n \"total_calls\": total_calls,\n \"total_cost_usd\": round(total_cost, 6),\n \"monthly_start_iso\": time.strftime(\"%Y-%m-%d\", time.localtime(self._monthly_start)),\n \"by_model\": {\n name: {\n \"calls\": self._calls.get(name, 0),\n \"tokens\": self._tokens.get(name, 0),\n \"cost_usd\": round(self._cost.get(name, 0.0), 6),\n }\n for name in set(list(self._calls.keys()) + list(self._tokens.keys()) + list(self._cost.keys()))\n },\n }\n\n def reset(self):\n self._calls.clear()\n self._tokens.clear()\n self._cost.clear()\n self._monthly_start = time.time()\n\n\nclass ModelRegistry:\n \"\"\"\n 模型注册表:管理所有可用 LLM 模型。\n \"\"\"\n\n def __init__(self):\n self._models: Dict[str, ModelConfig] = {}\n self._load_from_env()\n\n def add_model(\n self,\n name: str,\n endpoint: str,\n api_key: str,\n cost_per_1m: float,\n tags: Optional[List[str]] = None,\n context_length: int = 4096,\n ) -> bool:\n \"\"\"注册新模型\"\"\"\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=tags or [],\n context_length=context_length,\n )\n logger.info(f\"[ModelRegistry] Registered: {name} (${cost_per_1m}/1M)\")\n return True\n\n def remove_model(self, name: str) -> bool:\n if name in self._models:\n del self._models[name]\n return True\n return False\n\n def get_model(self, name: str) -> Optional[ModelConfig]:\n return self._models.get(name)\n\n def get_models(self) -> List[Dict]:\n \"\"\"返回所有模型配置(字典形式)\"\"\"\n return [\n {\n \"name\": m.name,\n \"endpoint\": m.endpoint,\n \"cost_per_1m\": m.cost_per_1m,\n \"tags\": m.tags,\n \"context_length\": m.context_length,\n }\n for m in self._models.values()\n ]\n\n def _load_from_env(self):\n \"\"\"从环境变量和 CredentialVault 自动加载已配置模型\"\"\"\n vault = CredentialVault()\n\n # OpenAI\n openai_key = os.environ.get(\"OPENAI_API_KEY\") or vault.get(\"openai_api_key\") or \"\"\n if openai_key:\n self.add_model(\n name=\"gpt-4o\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=5.0,\n tags=[\"strong\", \"code\", \"analysis\", \"conversation\", \"safety\"],\n context_length=128000,\n )\n self.add_model(\n name=\"gpt-4o-mini\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=0.15,\n tags=[\"cheap\", \"summary\", \"conversation\"],\n context_length=128000,\n )\n\n # DeepSeek\n deepseek_key = os.environ.get(\"DEEPSEEK_API_KEY\") or vault.get(\"deepseek_api_key\") or \"\"\n if deepseek_key:\n self.add_model(\n name=\"deepseek-chat\",\n endpoint=\"https://api.deepseek.com/v1\",\n api_key=deepseek_key,\n cost_per_1m=0.14,\n tags=[\"cheap\", \"code\", \"analysis\", \"conversation\"],\n context_length=64000,\n )\n\n # Claude\n claude_key = os.environ.get(\"ANTHROPIC_API_KEY\") or vault.get(\"anthropic_api_key\") or \"\"\n if claude_key:\n self.add_model(\n name=\"claude-3-5-sonnet\",\n endpoint=\"https://api.anthropic.com/v1\",\n api_key=claude_key,\n cost_per_1m=3.0,\n tags=[\"strong\", \"code\", \"analysis\", \"safety\"],\n context_length=200000,\n )\n\n\nclass SmartRouter:\n \"\"\"\n 多模型智能路由。\n\n 路由策略:\n 1. 任务分类(基于关键词启发式 + 可选 LLM 分类)\n 2. 模型匹配(tags 匹配)\n 3. 成本排序(cost_per_1m 升序)\n 4. 熔断检查(跳过故障模型)\n 5. 预算约束(排除超预算模型)\n 6. 选择最优模型\n \"\"\"\n\n # 任务分类关键词映射\n TASK_CLASSIFICATION = {\n \"code\": [\"代码\", \"code\", \"编程\", \"programming\", \"算法\", \"algorithm\", \"debug\", \"函数\", \"class\", \"optimize\", \"优化\", \"query\", \"sql\", \"database\"],\n \"analysis\": [\"分析\", \"analysis\", \"评估\", \"evaluate\", \"比较\", \"compare\", \"研究\", \"research\"],\n \"summary\": [\"总结\", \"summary\", \"摘要\", \"abstract\", \"概括\", \"overview\", \"提炼\"],\n \"conversation\": [\"对话\", \"conversation\", \"聊天\", \"chat\", \"问答\", \"q&a\", \"交流\"],\n \"safety\": [\"安全\", \"safety\", \"审核\", \"audit\", \"风险\", \"risk\", \"合规\", \"compliance\"],\n }\n\n def __init__(self, registry: Optional[ModelRegistry] = None):\n self.registry = registry or ModelRegistry()\n self.tracker = CostTracker()\n self._strategy: str = \"balanced\"\n\n @property\n def strategy(self) -> str:\n return self._strategy\n\n @strategy.setter\n def strategy(self, value: str):\n if value not in (\"cost_first\", \"quality_first\", \"balanced\"):\n raise ValueError(f\"Unknown strategy: {value}\")\n self._strategy = value\n\n def _classify_task(self, task_description: str) -> List[str]:\n \"\"\"基于关键词对任务进行分类\"\"\"\n task_lower = task_description.lower()\n categories = []\n for category, keywords in self.TASK_CLASSIFICATION.items():\n if any(kw.lower() in task_lower for kw in keywords):\n categories.append(category)\n if not categories:\n categories.append(\"conversation\") # 默认分类\n return categories\n\n def _match_models(self, categories: List[str], context_length_required: int = 0) -> List[ModelConfig]:\n \"\"\"根据分类匹配候选模型\"\"\"\n candidates = []\n for model in self.registry._models.values():\n # 上下文长度检查\n if context_length_required > 0 and model.context_length < context_length_required:\n continue\n # Tags 匹配(至少匹配一个分类)\n if any(cat in model.tags for cat in categories):\n candidates.append(model)\n return candidates\n\n def _filter_available(\n self,\n candidates: List[ModelConfig],\n max_cost_budget: Optional[float] = None,\n ) -> List[ModelConfig]:\n \"\"\"应用预算和熔断过滤\"\"\"\n available = []\n for model in candidates:\n if max_cost_budget is not None and model.cost_per_1m > max_cost_budget:\n continue\n cb = get_circuit_breaker(f\"llm:{model.name}\")\n if cb.is_open():\n logger.warning(f\"[SmartRouter] Model {model.name} circuit OPEN, skipping\")\n continue\n available.append(model)\n return available\n\n def _sort_by_strategy(self, models: List[ModelConfig], strategy: str) -> List[ModelConfig]:\n \"\"\"根据策略排序模型\"\"\"\n if strategy == \"cost_first\":\n models.sort(key=lambda m: m.cost_per_1m)\n elif strategy == \"quality_first\":\n models.sort(key=lambda m: (-m.context_length, m.cost_per_1m))\n else: # balanced\n models.sort(key=lambda m: m.context_length / (m.cost_per_1m + 0.01), reverse=True)\n return models\n\n def _estimate_cost(self, model: ModelConfig, context_length_required: int = 0) -> float:\n \"\"\"估算单次调用成本(假设 1K 输出 tokens)\"\"\"\n input_tokens = max(context_length_required, 1000)\n output_tokens = 1000\n total = input_tokens + output_tokens\n return round((total / 1_000_000) * model.cost_per_1m, 8)\n\n def route(\n self,\n task_description: str,\n context_length_required: int = 0,\n max_cost_budget: Optional[float] = None,\n strategy: str = \"balanced\",\n ) -> Optional[Dict]:\n \"\"\"\n 同步路由到最优模型。\n 返回模型配置字典 + 预估成本,或 None(无可用模型)。\n \"\"\"\n categories = self._classify_task(task_description)\n candidates = self._match_models(categories, context_length_required)\n\n if not candidates:\n candidates = list(self.registry._models.values())\n\n available = self._filter_available(candidates, max_cost_budget)\n\n if not available:\n return None\n\n available = self._sort_by_strategy(available, strategy)\n winner = available[0]\n\n estimated_cost = self._estimate_cost(winner, context_length_required)\n\n return {\n \"name\": winner.name,\n \"endpoint\": winner.endpoint,\n \"api_key\": winner.api_key,\n \"cost_per_1m\": winner.cost_per_1m,\n \"tags\": winner.tags,\n \"context_length\": winner.context_length,\n \"matched_categories\": categories,\n \"strategy\": strategy,\n \"estimated_cost\": estimated_cost,\n }\n\n async def aroute(\n self,\n task_description: str,\n context_length_required: int = 0,\n budget_limit: Optional[float] = None,\n strategy: str = \"balanced\",\n ) -> Optional[Dict]:\n \"\"\"\n 异步路由到最优模型。\n 与 sync route 行为一致,预留 LLM-based classification 的异步扩展点。\n \"\"\"\n # TODO: 未来可在此接入异步 LLM 分类器\n result = self.route(\n task_description=task_description,\n context_length_required=context_length_required,\n max_cost_budget=budget_limit,\n strategy=strategy,\n )\n return result\n\n def record_failure(self, model_name: str):\n \"\"\"记录模型调用失败(供熔断器使用)\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_failure()\n\n def record_success(self, model_name: str):\n \"\"\"记录模型调用成功\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_success()\n\n def get_stats(self) -> Dict[str, Any]:\n \"\"\"返回路由统计信息\"\"\"\n return self.tracker.get_stats()\n\n def reset_stats(self):\n \"\"\"重置统计\"\"\"\n self.tracker.reset()\n\n def get_circuit_status(self) -> Dict[str, Any]:\n \"\"\"返回所有模型的熔断器状态\"\"\"\n status = {}\n for name in self.registry._models:\n cb = get_circuit_breaker(f\"llm:{name}\")\n status[name] = {\n \"state\": cb.state.value,\n \"failure_count\": cb._failure_count,\n \"is_open\": cb.is_open(),\n }\n return status\n\n\n# =============================================================================\n# MCP Tool Registration\n# =============================================================================\n\ndef register_llm_routing_tools(mcp: Any):\n \"\"\"向 FastMCP 实例注册 LLM 路由相关 Tools。\"\"\"\n from core.llm.smart_router import ModelRegistry, SmartRouter\n\n _llm_registry = ModelRegistry()\n _smart_router = SmartRouter(_llm_registry)\n\n @mcp.tool(\"llm.route_task\")\n def llm_route_task(\n task_description: str,\n context_length: int = 0,\n budget_limit: float = 0.0,\n strategy: str = \"balanced\",\n ) -> str:\n \"\"\"\n 智能路由:根据任务描述推荐最优 LLM 模型,并返回预估成本。\n\n Args:\n task_description: 任务描述(如\"总结这篇文章\"、\"优化复杂数据库查询\")\n context_length: 需要的上下文长度(默认 0 表示不限制)\n budget_limit: 预算上限($/1M tokens),0 表示无限制\n strategy: 路由策略:cost_first | quality_first | balanced\n\n Returns:\n JSON 字符串,包含 recommended_model、estimated_cost、matched_categories\n \"\"\"\n try:\n budget = budget_limit if budget_limit > 0 else None\n result = _smart_router.route(\n task_description=task_description,\n context_length_required=context_length,\n max_cost_budget=budget,\n strategy=strategy,\n )\n if result:\n return json.dumps({\n \"success\": True,\n \"recommended_model\": result[\"name\"],\n \"endpoint\": result[\"endpoint\"],\n \"estimated_cost_usd\": result[\"estimated_cost\"],\n \"cost_per_1m\": result[\"cost_per_1m\"],\n \"context_length\": result[\"context_length\"],\n \"matched_categories\": result[\"matched_categories\"],\n \"strategy\": result[\"strategy\"],\n }, ensure_ascii=False)\n return json.dumps({\"success\": False, \"error\": \"No available model\"}, ensure_ascii=False)\n except Exception as e:\n logger.exception(\"llm.route_task failed\")\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n @mcp.tool(\"llm.list_models\")\n def llm_list_models() -> str:\n \"\"\"列出所有已注册模型。\"\"\"\n try:\n models = _llm_registry.get_models()\n return json.dumps({\"success\": True, \"models\": models}, ensure_ascii=False)\n except Exception as e:\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n @mcp.tool(\"llm.get_stats\")\n def llm_get_stats() -> str:\n \"\"\"获取 LLM 路由调用统计。\"\"\"\n try:\n stats = _smart_router.get_stats()\n return json.dumps({\"success\": True, \"stats\": stats}, ensure_ascii=False)\n except Exception as e:\n return json.dumps({\"success\": False, \"error\": str(e)}, ensure_ascii=False)\n\n logger.info(\"LLM routing MCP tools registered: llm.route_task, llm.list_models, llm.get_stats\")\n"}, "80c2b7b158aeff08411f9aeead7ccc0469a3887f": {"t": 1785758510.794128, "n": 7743, "h": "e14f83a951e8c302", "c": "\"\"\"\n多模型智能路由与成本控制\n\nSmartRouter + ModelRegistry\n\"\"\"\n\nimport json\nimport logging\nimport os\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, List, Optional\n\nfrom core.resilience import get_circuit_breaker\nfrom core.security.credential_vault import CredentialVault\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass ModelConfig:\n \"\"\"模型配置\"\"\"\n name: str\n endpoint: str\n api_key: str\n cost_per_1m: float # 每百万 token 成本(美元)\n tags: List[str] = field(default_factory=list)\n context_length: int = 4096\n\n\nclass ModelRegistry:\n \"\"\"\n 模型注册表:管理所有可用 LLM 模型。\n \"\"\"\n\n def __init__(self):\n self._models: Dict[str, ModelConfig] = {}\n self._load_from_env()\n\n def add_model(\n self,\n name: str,\n endpoint: str,\n api_key: str,\n cost_per_1m: float,\n tags: Optional[List[str]] = None,\n context_length: int = 4096,\n ) -> bool:\n \"\"\"注册新模型\"\"\"\n self._models[name] = ModelConfig(\n name=name,\n endpoint=endpoint,\n api_key=api_key,\n cost_per_1m=cost_per_1m,\n tags=tags or [],\n context_length=context_length,\n )\n logger.info(f\"[ModelRegistry] Registered: {name} (${cost_per_1m}/1M)\")\n return True\n\n def remove_model(self, name: str) -> bool:\n if name in self._models:\n del self._models[name]\n return True\n return False\n\n def get_model(self, name: str) -> Optional[ModelConfig]:\n return self._models.get(name)\n\n def get_models(self) -> List[Dict]:\n \"\"\"返回所有模型配置(字典形式)\"\"\"\n return [\n {\n \"name\": m.name,\n \"endpoint\": m.endpoint,\n \"cost_per_1m\": m.cost_per_1m,\n \"tags\": m.tags,\n \"context_length\": m.context_length,\n }\n for m in self._models.values()\n ]\n\n def _load_from_env(self):\n \"\"\"从环境变量和 CredentialVault 自动加载已配置模型\"\"\"\n # 默认加载常见模型占位(实际 API Key 从 vault 或 env 获取)\n vault = CredentialVault()\n\n # OpenAI\n openai_key = os.environ.get(\"OPENAI_API_KEY\") or vault.get(\"openai_api_key\") or \"\"\n if openai_key:\n self.add_model(\n name=\"gpt-4o\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=5.0,\n tags=[\"strong\", \"code\", \"analysis\", \"conversation\", \"safety\"],\n context_length=128000,\n )\n self.add_model(\n name=\"gpt-4o-mini\",\n endpoint=\"https://api.openai.com/v1\",\n api_key=openai_key,\n cost_per_1m=0.15,\n tags=[\"cheap\", \"summary\", \"conversation\"],\n context_length=128000,\n )\n\n # DeepSeek\n deepseek_key = os.environ.get(\"DEEPSEEK_API_KEY\") or vault.get(\"deepseek_api_key\") or \"\"\n if deepseek_key:\n self.add_model(\n name=\"deepseek-chat\",\n endpoint=\"https://api.deepseek.com/v1\",\n api_key=deepseek_key,\n cost_per_1m=0.14,\n tags=[\"cheap\", \"code\", \"analysis\", \"conversation\"],\n context_length=64000,\n )\n\n # Claude\n claude_key = os.environ.get(\"ANTHROPIC_API_KEY\") or vault.get(\"anthropic_api_key\") or \"\"\n if claude_key:\n self.add_model(\n name=\"claude-3-5-sonnet\",\n endpoint=\"https://api.anthropic.com/v1\",\n api_key=claude_key,\n cost_per_1m=3.0,\n tags=[\"strong\", \"code\", \"analysis\", \"safety\"],\n context_length=200000,\n )\n\n\nclass SmartRouter:\n \"\"\"\n 多模型智能路由。\n\n 路由策略:\n 1. 任务分类(基于关键词启发式)\n 2. 模型匹配(tags 匹配)\n 3. 成本排序(cost_per_1m 升序)\n 4. 熔断检查(跳过故障模型)\n 5. 选择最优模型\n \"\"\"\n\n # 任务分类关键词映射\n TASK_CLASSIFICATION = {\n \"code\": [\"代码\", \"code\", \"编程\", \"programming\", \"算法\", \"algorithm\", \"debug\", \"函数\", \"class\"],\n \"analysis\": [\"分析\", \"analysis\", \"评估\", \"evaluate\", \"比较\", \"compare\", \"研究\", \"research\"],\n \"summary\": [\"总结\", \"summary\", \"摘要\", \"abstract\", \"概括\", \"overview\", \"提炼\"],\n \"conversation\": [\"对话\", \"conversation\", \"聊天\", \"chat\", \"问答\", \"q&a\", \"交流\"],\n \"safety\": [\"安全\", \"safety\", \"审核\", \"audit\", \"风险\", \"risk\", \"合规\", \"compliance\"],\n }\n\n def __init__(self, registry: Optional[ModelRegistry] = None):\n self.registry = registry or ModelRegistry()\n\n def _classify_task(self, task_description: str) -> List[str]:\n \"\"\"基于关键词对任务进行分类\"\"\"\n task_lower = task_description.lower()\n categories = []\n for category, keywords in self.TASK_CLASSIFICATION.items():\n if any(kw.lower() in task_lower for kw in keywords):\n categories.append(category)\n if not categories:\n categories.append(\"conversation\") # 默认分类\n return categories\n\n def _match_models(self, categories: List[str], context_length_required: int = 0) -> List[ModelConfig]:\n \"\"\"根据分类匹配候选模型\"\"\"\n candidates = []\n for model in self.registry._models.values():\n # 上下文长度检查\n if context_length_required > 0 and model.context_length < context_length_required:\n continue\n # Tags 匹配(至少匹配一个分类)\n if any(cat in model.tags for cat in categories):\n candidates.append(model)\n return candidates\n\n def route(\n self,\n task_description: str,\n context_length_required: int = 0,\n max_cost_budget: Optional[float] = None,\n strategy: str = \"balanced\", # \"cost_first\" | \"quality_first\" | \"balanced\"\n ) -> Optional[Dict]:\n \"\"\"\n 路由到最优模型。\n 返回模型配置字典,或 None(无可用模型)。\n \"\"\"\n categories = self._classify_task(task_description)\n candidates = self._match_models(categories, context_length_required)\n\n if not candidates:\n # 无匹配时回退到所有模型\n candidates = list(self.registry._models.values())\n\n # 熔断检查 + 成本过滤\n available = []\n for model in candidates:\n # 成本预算检查\n if max_cost_budget is not None and model.cost_per_1m > max_cost_budget:\n continue\n\n # 熔断检查\n cb = get_circuit_breaker(f\"llm:{model.name}\")\n if cb.is_open():\n logger.warning(f\"[SmartRouter] Model {model.name} circuit OPEN, skipping\")\n continue\n\n available.append(model)\n\n if not available:\n return None\n\n # 根据策略排序\n if strategy == \"cost_first\":\n available.sort(key=lambda m: m.cost_per_1m)\n elif strategy == \"quality_first\":\n # 质量优先:context_length 降序 + cost 次要\n available.sort(key=lambda m: (-m.context_length, m.cost_per_1m))\n else: # balanced\n # 平衡模式:综合评分 = context_length / (cost_per_1m + 0.01)\n available.sort(key=lambda m: m.context_length / (m.cost_per_1m + 0.01), reverse=True)\n\n winner = available[0]\n return {\n \"name\": winner.name,\n \"endpoint\": winner.endpoint,\n \"api_key\": winner.api_key,\n \"cost_per_1m\": winner.cost_per_1m,\n \"tags\": winner.tags,\n \"context_length\": winner.context_length,\n \"matched_categories\": categories,\n \"strategy\": strategy,\n }\n\n def record_failure(self, model_name: str):\n \"\"\"记录模型调用失败(供熔断器使用)\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_failure()\n\n def record_success(self, model_name: str):\n \"\"\"记录模型调用成功\"\"\"\n cb = get_circuit_breaker(f\"llm:{model_name}\")\n cb.record_success()\n"}}}}