218 lines
9.0 KiB
Python
218 lines
9.0 KiB
Python
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
|
|
|