1 line
70 KiB
JSON
1 line
70 KiB
JSON
{"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"}}}} |