from __future__ import annotations from .shared import * class LLMOpenclawRoutingMixin: def _mixed_routing_enabled(self) -> bool: return _env_bool("LLM_MIXED_ROUTING_ENABLED", False) def _openclaw_routing_enabled(self) -> bool: return _env_bool("OPENCLAW_ROUTING_ENABLED", False) def _openclaw_control_plane_only(self) -> bool: return _env_bool("OPENCLAW_CONTROL_PLANE_ONLY", False) def _ensure_openclaw_control_plane_engine(self, engine: dict[str, Any]) -> dict[str, Any]: runtime = dict(engine or {}) runtime["source_provider"] = str(runtime.get("source_provider") or runtime.get("provider") or "").strip().lower() return runtime def _openclaw_rules_path(self) -> str: return str(resolve_model_config_path()) def _load_openclaw_routing_bundle(self) -> dict[str, Any]: now = time.time() if self._openclaw_routing_cache and self._openclaw_routing_cache[0] > now: return dict(self._openclaw_routing_cache[1]) path = Path(self._openclaw_rules_path()) bundle: dict[str, Any] = load_model_config(path=path) self._openclaw_routing_cache = (now + OPENCLAW_ROUTING_CACHE_TTL_SECONDS, dict(bundle)) return dict(bundle) @staticmethod def _rule_matches_function(rule: dict[str, Any], function_name: str) -> bool: if bool(rule.get("match_all", False)): return True fn = str(function_name or "").strip().lower() if not fn: return False exact = str(rule.get("function_exact") or "").strip().lower() if exact and fn == exact: return True prefix = str(rule.get("function_prefix") or "").strip().lower() if prefix == "*" or prefix == "all": return True if prefix and fn.startswith(prefix): return True match_any = rule.get("match_any") if isinstance(match_any, list): for hint in match_any: token = str(hint or "").strip().lower() if token and token in fn: return True return False def _match_openclaw_rule( self, function_name: str, *, tier_filter: Optional[set[str]] = None, ) -> Optional[dict[str, Any]]: if not self._openclaw_routing_enabled(): return None bundle = self._load_openclaw_routing_bundle() rules = bundle.get("rules") if isinstance(bundle.get("rules"), list) else [] if not rules: return None indexed: list[tuple[int, int, dict[str, Any]]] = [] for idx, row in enumerate(rules): if not isinstance(row, dict): continue if not bool(row.get("enabled", True)): continue if tier_filter: tier_name = str(row.get("tier") or "").strip().lower() if tier_name not in tier_filter: continue if not self._rule_matches_function(row, function_name): continue priority = int(row.get("priority") or 0) indexed.append((priority, -idx, row)) if not indexed: return None indexed.sort(reverse=True) return dict(indexed[0][2]) def _resolve_openclaw_engine( self, *, function_name: str, engines: list[dict[str, Any]], active_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: rule = self._match_openclaw_rule(function_name, tier_filter={"low", "high"}) if not rule: return None routed = self._resolve_engine_from_rule( rule=rule, engines=engines, active_engine=active_engine, ) if routed is None: return None _runtime_log( "OpenClaw路由", ( "规则命中:" f"function={function_name}, provider={routed.get('provider')}, " f"model={routed.get('model')}, rule={rule.get('name') or 'unnamed'}" ), ) return routed def _resolve_engine_from_rule( self, *, rule: dict[str, Any], engines: list[dict[str, Any]], active_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: tier = str(rule.get("tier") or "").strip().lower() provider_raw = str(rule.get("provider") or "").strip().lower() engine_id = str(rule.get("engine_id") or "").strip() model_override = str(rule.get("model") or "").strip() base_url_override = str(rule.get("base_url") or "").strip().rstrip("/") api_env_override = str(rule.get("api_env") or "").strip() routed: Optional[dict[str, Any]] = None if engine_id: for item in engines: if str(item.get("id") or "").strip() == engine_id: routed = self._normalize_engine_runtime(item) break if routed is None and provider_raw: try: provider = _normalize_provider(provider_raw) except Exception: provider = "" if provider: for item in engines: if str(item.get("provider") or "").strip().lower() == provider: routed = self._normalize_engine_runtime(item) break if routed is None: try: routed = self._env_provider_runtime(provider) except Exception: routed = None if routed is None and tier in {"high", "low", "safety"}: routed = self._resolve_tier_engine( tier=tier, engines=engines, active_engine=active_engine, ) if routed is None: return None if model_override: routed["model"] = model_override if base_url_override: routed["base_url"] = base_url_override if api_env_override: env_api_key = str(os.getenv(api_env_override, "") or "").strip() if env_api_key: routed["api_key"] = env_api_key routed["api_env"] = api_env_override return routed def _resolve_function_tier(self, function_name: str) -> str: fn = str(function_name or "").strip().lower() if not fn: return str(os.getenv("LLM_ROUTING_DEFAULT_TIER", "active") or "active").strip().lower() high_hints = _parse_csv_items(os.getenv("LLM_ROUTING_HIGH_FUNCTIONS", "")) low_hints = _parse_csv_items(os.getenv("LLM_ROUTING_LOW_FUNCTIONS", "")) high_pool = high_hints or DEFAULT_HIGH_TIER_HINTS low_pool = low_hints or DEFAULT_LOW_TIER_HINTS if any(hint and hint in fn for hint in high_pool): return "high" if any(hint and hint in fn for hint in low_pool): return "low" default_tier = str(os.getenv("LLM_ROUTING_DEFAULT_TIER", "active") or "active").strip().lower() if default_tier != "active": return default_tier # mixed routing 开启时,未知函数默认走 high,避免被 active 引擎“全吞”。 return "high" if self._mixed_routing_enabled() else "active"