from __future__ import annotations from .shared import * class LLMTierEngineSelectionMixin: @staticmethod def _is_same_runtime_engine(a: dict[str, Any], b: dict[str, Any]) -> bool: return ( str(a.get("provider") or "").strip().lower() == str(b.get("provider") or "").strip().lower() and str(a.get("model") or "").strip().lower() == str(b.get("model") or "").strip().lower() and str(a.get("base_url") or "").strip().rstrip("/").lower() == str(b.get("base_url") or "").strip().rstrip("/").lower() ) async def _resolve_high_tier_engine_for_escalation( self, db: AsyncSession, *, tenant_id: int, primary_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) active_engine = self._resolve_active_engine_from_bundle(bundle) engines: list[dict[str, Any]] = [e for e in bundle.get("engines", []) if isinstance(e, dict)] routed = self._resolve_tier_engine( tier="high", engines=engines, active_engine=active_engine, ) if not routed: return None if self._is_same_runtime_engine(routed, primary_engine): return None provider = str(routed.get("provider") or "").strip().lower() api_key = str(routed.get("api_key") or "").strip() if provider != "ollama" and not api_key: return None return routed async def _resolve_safety_tier_engine( self, db: AsyncSession, *, tenant_id: int, function_name: str, primary_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) active_engine = self._resolve_active_engine_from_bundle(bundle) engines: list[dict[str, Any]] = [e for e in bundle.get("engines", []) if isinstance(e, dict)] safety_rule = self._match_openclaw_rule(function_name, tier_filter={"safety"}) routed: Optional[dict[str, Any]] = None if safety_rule is not None: routed = self._resolve_engine_from_rule( rule=safety_rule, engines=engines, active_engine=active_engine, ) if routed is None: routed = self._resolve_tier_engine( tier="safety", engines=engines, active_engine=active_engine, ) if not routed: return None routed = self._ensure_openclaw_control_plane_engine(routed) if self._is_same_runtime_engine(routed, primary_engine): return None provider = str(routed.get("provider") or "").strip().lower() api_key = str(routed.get("api_key") or "").strip() if provider != "ollama" and not api_key: return None return routed async def _try_safety_engine( self, db: AsyncSession, *, tenant_id: int, function_name: str, prompt: str, system_prompt: str, temperature: float, max_tokens: int, primary_engine: dict[str, Any], reason: str, ) -> tuple[str, Optional[dict[str, Any]]]: safety_engine = await self._resolve_safety_tier_engine( db, tenant_id=tenant_id, function_name=function_name, primary_engine=primary_engine, ) if safety_engine is None: _runtime_log( "LLM路由", f"Safety 引擎不可用,无法触发兜底:function={function_name}, reason={reason}", ) return "", None safety_provider = str(safety_engine.get("provider") or "") safety_model = str(safety_engine.get("model") or "") _runtime_log( "LLM路由", ( "触发 Safety 兜底:" f"function={function_name}, provider={safety_provider}, model={safety_model}, reason={reason}" ), ) try: text = await self._chat_completion( provider=safety_provider, prompt=prompt, system_prompt=system_prompt, api_key=str(safety_engine.get("api_key") or ""), base_url=str(safety_engine.get("base_url") or ""), model=safety_model, temperature=temperature, max_tokens=max_tokens, function_name=function_name, ) if not text.strip(): raise ValueError("safety_engine_empty_response") if self._is_invalid_structured_output(function_name=function_name, response_text=text): raise ValueError("safety_engine_invalid_structured_output") return text, safety_engine except Exception as exc: _runtime_log("LLM路由", f"Safety 兜底失败:function={function_name}, error={exc}") return "", None def _pick_tier_fallback_engine( self, *, tier: str, engines: list[dict[str, Any]], active_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: for item in engines: candidate = self._normalize_engine_runtime(item) provider = str(candidate.get("provider") or "") api_key = str(candidate.get("api_key") or "").strip() if provider != "ollama" and not api_key: continue if tier == "high" and provider == "ollama": continue if str(candidate.get("id") or "") == str(active_engine.get("id") or ""): continue _runtime_log( "LLM路由", ( "分层路由回退引擎生效:" f"tier={tier}, provider={provider}, model={candidate.get('model')}" ), ) return candidate return None def _resolve_tier_engine( self, *, tier: str, engines: list[dict[str, Any]], active_engine: dict[str, Any], ) -> Optional[dict[str, Any]]: tier = str(tier or "").strip().lower() upper = tier.upper() default_provider_by_tier = { "low": "glm", "high": "openai", "safety": "glm", } provider_env = str( os.getenv( f"LLM_ROUTING_{upper}_PROVIDER", default_provider_by_tier.get( tier, os.getenv("LLM_PROVIDER", "glm"), ), ) or "" ).strip() engine_id = str(os.getenv(f"LLM_ROUTING_{upper}_ENGINE_ID", "") or "").strip() model_override = str(os.getenv(f"LLM_ROUTING_{upper}_MODEL", "") or "").strip() base_url_override = str(os.getenv(f"LLM_ROUTING_{upper}_BASE_URL", "") or "").strip().rstrip("/") routed: Optional[dict[str, Any]] = None if engine_id: for item in engines: if str(item.get("id") or "") == engine_id: routed = self._normalize_engine_runtime(item) break if routed is None and provider_env: try: provider = _normalize_provider(provider_env) 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: return None provider = str(routed.get("provider") or "").strip().lower() if model_override: routed["model"] = model_override elif self._openclaw_routing_enabled() and provider == "openai": default_model = DEFAULT_TIER_MODEL_OVERRIDES.get(tier) if default_model: routed["model"] = default_model if base_url_override: routed["base_url"] = base_url_override elif self._openclaw_routing_enabled() and provider == "openai": configured_openai_base = str(os.getenv("OPENAI_BASE_URL", "") or "").strip().rstrip("/") routed["base_url"] = configured_openai_base or "https://api.openai.com/v1" if str(routed.get("id") or "") != str(active_engine.get("id") or ""): _runtime_log( "LLM路由", ( "混合路由已命中:" f"层级={tier}, provider={routed.get('provider')}, model={routed.get('model')}" ), ) return self._ensure_openclaw_control_plane_engine(routed) async def _resolve_engine_for_test( self, db: AsyncSession, *, tenant_id: int, engine_id: Optional[str], ) -> dict[str, Any]: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) settings = self._normalize_engine_settings(bundle.get("settings")) if not engine_id: engine = self._resolve_active_engine_from_bundle(bundle) return self._ensure_engine_allowed_for_test( engine=engine, settings=settings, engine_label="当前激活引擎", ) engines: list[dict[str, Any]] = [e for e in bundle.get("engines", []) if isinstance(e, dict)] for item in engines: if str(item.get("id") or "") == str(engine_id): engine = self._normalize_engine_runtime(item) return self._ensure_engine_allowed_for_test( engine=engine, settings=settings, engine_label="目标引擎", ) raise ValueError("engine 不存在")