from __future__ import annotations from .shared import * class LLMTierEscalationMixin: def _low_to_high_escalation_enabled(self) -> bool: return _env_bool("LLM_LOW_TO_HIGH_ESCALATION_ENABLED", True) def _is_low_tier_function(self, function_name: str) -> bool: rule = self._match_openclaw_rule(function_name) if rule is not None: tier = str(rule.get("tier") or "").strip().lower() if tier: return tier == "low" return self._resolve_function_tier(function_name) == "low" @staticmethod def _looks_like_unanswerable_response(text: str) -> bool: cleaned = str(text or "").strip() lowered = cleaned.lower() if not lowered: return True if len(cleaned) <= 8: return True return any(marker in lowered for marker in LOW_TO_HIGH_ESCALATION_FALLBACK_MARKERS) def _should_escalate_low_to_high(self, *, function_name: str, response_text: str) -> bool: if not self._low_to_high_escalation_enabled(): return False if not self._is_low_tier_function(function_name): return False return self._looks_like_unanswerable_response(response_text) @staticmethod def _is_glm_47_model(model_name: str) -> bool: normalized = re.sub(r"[\s_]+", "", str(model_name or "")).lower() if not normalized: return False return "glm" in normalized and "4.7" in normalized def _should_retry_glm47_with_high_tier( self, *, function_name: str, failed_engine: dict[str, Any], ) -> bool: if not self._low_to_high_escalation_enabled(): return False failed_model = str(failed_engine.get("model") or "").strip() if self._is_glm_47_model(failed_model): return True # 兜底兼容:低阶函数遇到 4.x GLM 也允许升级高阶重试。 if self._is_low_tier_function(function_name): lowered = failed_model.lower() return "glm" in lowered and "4." in lowered return False async def _retry_with_high_tier_after_low_model_failure( self, *, db: AsyncSession, tenant_id: int, function_name: str, failed_engine: dict[str, Any], prompt: str, system_prompt: str, temperature: float, max_tokens: int, response_mode: str, error: Exception, ) -> tuple[Optional[str], Optional[dict[str, Any]]]: if not self._should_retry_glm47_with_high_tier(function_name=function_name, failed_engine=failed_engine): return None, None try: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) engines = [item for item in bundle.get("engines", []) if isinstance(item, dict)] active_engine = self._resolve_active_engine_from_bundle(bundle) high_engine = self._resolve_tier_engine( tier="high", engines=engines, active_engine=active_engine, ) if not isinstance(high_engine, dict): return None, None high_engine = self._ensure_engine_allowed_for_function( engine=high_engine, function_name=function_name, settings=self._normalize_engine_settings(bundle.get("settings")), engine_label="high_tier_failover", ) failed_provider = str(failed_engine.get("provider") or "").strip().lower() failed_model = str(failed_engine.get("model") or "").strip() high_provider = str(high_engine.get("provider") or "").strip().lower() high_model = str(high_engine.get("model") or "").strip() if failed_provider == high_provider and failed_model == high_model: return None, None _runtime_log( "LLM路由", ( "低阶模型失败自动升级:" f"function={function_name}, from={failed_provider}:{failed_model}, " f"to={high_provider}:{high_model}, reason={str(error)[:120]}" ), ) text = await self._chat_completion( provider=high_provider, prompt=prompt, system_prompt=system_prompt, api_key=str(high_engine.get("api_key") or ""), base_url=str(high_engine.get("base_url") or ""), model=high_model, temperature=temperature, max_tokens=max_tokens, function_name=function_name, response_mode=response_mode, ) if not text.strip(): raise ValueError("high_tier_empty_response") if self._is_invalid_structured_output(function_name=function_name, response_text=text): raise ValueError("high_tier_invalid_structured_output") return text, high_engine except Exception as retry_exc: _runtime_log( "LLM路由", ( "低阶模型失败升级高阶也失败:" f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}" ), ) return None, None async def _retry_structured_with_high_tier_after_low_model_failure( self, *, db: AsyncSession, tenant_id: int, function_name: str, failed_engine: dict[str, Any], prompt: str, system_prompt: str, temperature: float, max_tokens: int, response_mode: str, error: Exception, ) -> tuple[Optional[StructuredExtractionResult], Optional[dict[str, Any]]]: if not self._should_retry_glm47_with_high_tier(function_name=function_name, failed_engine=failed_engine): return None, None try: _, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id) engines = [item for item in bundle.get("engines", []) if isinstance(item, dict)] active_engine = self._resolve_active_engine_from_bundle(bundle) high_engine = self._resolve_tier_engine( tier="high", engines=engines, active_engine=active_engine, ) if not isinstance(high_engine, dict): return None, None high_engine = self._ensure_engine_allowed_for_function( engine=high_engine, function_name=function_name, settings=self._normalize_engine_settings(bundle.get("settings")), engine_label="high_tier_structured_failover", ) failed_provider = str(failed_engine.get("provider") or "").strip().lower() failed_model = str(failed_engine.get("model") or "").strip() high_provider = str(high_engine.get("provider") or "").strip().lower() high_model = str(high_engine.get("model") or "").strip() if failed_provider == high_provider and failed_model == high_model: return None, None _runtime_log( "LLM路由", ( "低阶结构化失败自动升级:" f"function={function_name}, from={failed_provider}:{failed_model}, " f"to={high_provider}:{high_model}, reason={str(error)[:120]}" ), ) completion = await self._chat_completion_raw( provider=high_provider, prompt=prompt, system_prompt=system_prompt, api_key=str(high_engine.get("api_key") or ""), base_url=str(high_engine.get("base_url") or ""), model=high_model, temperature=temperature, max_tokens=max_tokens, function_name=function_name, response_mode=response_mode, ) extraction = self._extract_structured_object_from_response( function_name=function_name, response=completion.raw_response, fallback_text=completion.text, provider=high_provider, model=high_model, ) if extraction.payload is None: raise ValueError(str(extraction.error_code or "structured_schema_invalid")) _runtime_log( "LLM结构化", ( f"function={function_name}, structured_source={extraction.source_field or 'none'}, " f"validation={extraction.validation}, fallback=true" ), ) return extraction, high_engine except Exception as retry_exc: _runtime_log( "LLM路由", ( "低阶结构化失败升级高阶也失败:" f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}" ), ) return None, None