728 lines
30 KiB
Python
728 lines
30 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)
|
||
|
||
@staticmethod
|
||
def _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()
|
||
)
|
||
|
||
def _call_is_low_tier_function(
|
||
self,
|
||
function_name: str,
|
||
*,
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> bool:
|
||
try:
|
||
return bool(self._is_low_tier_function(function_name, routing_meta=routing_meta))
|
||
except TypeError:
|
||
return bool(self._is_low_tier_function(function_name))
|
||
|
||
def _call_is_invalid_structured_output_for_retry(
|
||
self,
|
||
*,
|
||
function_name: str,
|
||
response_text: str,
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> bool:
|
||
wrapper = getattr(self, "_call_is_invalid_structured_output", None)
|
||
if callable(wrapper):
|
||
return bool(
|
||
wrapper(
|
||
function_name=function_name,
|
||
response_text=response_text,
|
||
routing_meta=routing_meta,
|
||
)
|
||
)
|
||
checker = getattr(self, "_is_invalid_structured_output", None)
|
||
if not callable(checker):
|
||
return False
|
||
try:
|
||
return bool(
|
||
checker(
|
||
function_name=function_name,
|
||
response_text=response_text,
|
||
routing_meta=routing_meta,
|
||
)
|
||
)
|
||
except TypeError:
|
||
return bool(
|
||
checker(
|
||
function_name=function_name,
|
||
response_text=response_text,
|
||
)
|
||
)
|
||
|
||
def _is_low_tier_function(self, function_name: str, routing_meta: dict[str, Any] | None = None) -> bool:
|
||
rule = self._match_control_plane_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, routing_meta=routing_meta) == "low"
|
||
|
||
@staticmethod
|
||
def _is_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)
|
||
|
||
@staticmethod
|
||
def _normalize_model_name(model_name: str) -> str:
|
||
return re.sub(r"[\s_]+", "", str(model_name or "")).lower()
|
||
|
||
@staticmethod
|
||
def _is_tier_default_model(model_name: str, *, tier: str) -> bool:
|
||
normalized = LLMTierEscalationMixin._normalize_model_name(model_name)
|
||
if not normalized:
|
||
return False
|
||
tier_name = str(tier or "").strip().lower()
|
||
expected = LLMTierEscalationMixin._normalize_model_name(
|
||
str(TIER_DEFAULTS.get(tier_name, {}).get("model") or "")
|
||
)
|
||
if not expected:
|
||
return False
|
||
return normalized == expected
|
||
|
||
def _should_escalate_low_to_high(
|
||
self,
|
||
*,
|
||
function_name: str,
|
||
response_text: str,
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> bool:
|
||
meta = _normalize_routing_meta(routing_meta)
|
||
if meta.get("allow_low_to_high_escalation") is False:
|
||
return False
|
||
if not self._low_to_high_escalation_enabled():
|
||
return False
|
||
if not self._call_is_low_tier_function(function_name, routing_meta=meta):
|
||
return False
|
||
return self._is_unanswerable_response(response_text)
|
||
|
||
def _should_retry_low_tier_with_high_tier(
|
||
self,
|
||
*,
|
||
function_name: str,
|
||
failed_engine: dict[str, Any],
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> bool:
|
||
meta = _normalize_routing_meta(routing_meta)
|
||
if meta.get("allow_low_to_high_escalation") is False:
|
||
return False
|
||
if not self._low_to_high_escalation_enabled():
|
||
return False
|
||
_ = failed_engine
|
||
return self._call_is_low_tier_function(function_name, routing_meta=meta)
|
||
|
||
def _prefer_high_tier_retry_engine(
|
||
self,
|
||
*,
|
||
bundle: dict[str, Any],
|
||
engines: list[dict[str, Any]],
|
||
function_name: str,
|
||
failed_engine: dict[str, Any],
|
||
current_engine: dict[str, Any],
|
||
) -> Optional[dict[str, Any]]:
|
||
if self._is_tier_default_model(str(current_engine.get("model") or ""), tier="high"):
|
||
return current_engine
|
||
settings = self._normalize_engine_settings(bundle.get("settings"))
|
||
for item in engines:
|
||
candidate = dict(item)
|
||
if not self._is_tier_default_model(str(candidate.get("model") or ""), tier="high"):
|
||
continue
|
||
if self._same_runtime_engine(candidate, failed_engine):
|
||
continue
|
||
try:
|
||
candidate = self._ensure_engine_allowed_for_function(
|
||
engine=candidate,
|
||
function_name=function_name,
|
||
settings=settings,
|
||
engine_label="forced_high_tier_retry",
|
||
)
|
||
except Exception as exc:
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"高阶重试候选被拦截:"
|
||
f"function={function_name}, error={type(exc).__name__}:{str(exc)[:180]}"
|
||
),
|
||
)
|
||
continue
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"高阶重试候选命中:"
|
||
f"function={function_name}, tier=high, decision=forced_high_retry"
|
||
),
|
||
)
|
||
return candidate
|
||
return None
|
||
|
||
def _match_control_plane_high_tier_rule_for_escalation(self, function_name: str) -> Optional[dict[str, Any]]:
|
||
bundle = self._load_control_plane_routing_bundle()
|
||
rule = find_matching_rule(
|
||
function_name,
|
||
tier_filter={"high"},
|
||
bundle=bundle,
|
||
)
|
||
if not isinstance(rule, dict):
|
||
_runtime_log("LLM路由", f"PMFlow 本地路由层高阶规则未命中:function={function_name}")
|
||
return None
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶规则候选:"
|
||
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, priority={rule.get('priority')}"
|
||
),
|
||
)
|
||
return rule
|
||
|
||
async def _resolve_control_plane_high_tier_engine_for_escalation(
|
||
self,
|
||
*,
|
||
bundle: dict[str, Any],
|
||
engines: list[dict[str, Any]],
|
||
active_engine: dict[str, Any],
|
||
function_name: str,
|
||
failed_engine: dict[str, Any],
|
||
slot_bundle: dict[str, Any] | None = None,
|
||
) -> Optional[dict[str, Any]]:
|
||
rule = self._match_control_plane_high_tier_rule_for_escalation(function_name)
|
||
if not isinstance(rule, dict):
|
||
return None
|
||
routed = self._resolve_engine_from_rule(
|
||
rule=rule,
|
||
engines=engines,
|
||
active_engine=active_engine,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
if not isinstance(routed, dict):
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶规则命中但未解析到可用引擎:"
|
||
f"function={function_name}, rule={rule.get('name') or 'unnamed'}"
|
||
),
|
||
)
|
||
return None
|
||
routed = self._ensure_control_plane_engine(routed)
|
||
if self._same_runtime_engine(routed, failed_engine):
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶升级命中但与失败引擎同源,跳过:"
|
||
f"function={function_name}, tier=high"
|
||
),
|
||
)
|
||
return None
|
||
try:
|
||
routed = self._ensure_engine_allowed_for_function(
|
||
engine=routed,
|
||
function_name=function_name,
|
||
settings=self._normalize_engine_settings(bundle.get("settings")),
|
||
engine_label="control_plane_high_tier_failover",
|
||
)
|
||
except Exception as exc:
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶升级被拦截:"
|
||
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, "
|
||
f"error={type(exc).__name__}:{str(exc)[:180]}"
|
||
),
|
||
)
|
||
return None
|
||
config_snapshot = build_routing_config_snapshot(bundle=self._load_control_plane_routing_bundle())
|
||
routed = _merge_engine_routing_meta(
|
||
routed,
|
||
tier_alias="high",
|
||
planned_tier="high",
|
||
executed_tier="high",
|
||
route_source="default",
|
||
route_rule_name=str(rule.get("name") or "").strip(),
|
||
rule_priority=int(rule.get("priority") or 0),
|
||
routing_decision="high_tier_retry_candidate",
|
||
routing_config_version=config_snapshot.get("routing_config_version"),
|
||
routing_config_updated_at=config_snapshot.get("routing_config_updated_at"),
|
||
)
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶升级命中:"
|
||
f"function={function_name}, rule={rule.get('name') or 'unnamed'}, tier=high"
|
||
),
|
||
)
|
||
return routed
|
||
|
||
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,
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> tuple[Optional[str], Optional[dict[str, Any]]]:
|
||
if not self._should_retry_low_tier_with_high_tier(
|
||
function_name=function_name,
|
||
failed_engine=failed_engine,
|
||
routing_meta=routing_meta,
|
||
):
|
||
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) and self._engine_enabled(item)
|
||
]
|
||
active_engine = self._resolve_active_engine_from_bundle(bundle)
|
||
slot_bundle = await self._load_capability_slot_bundle(db)
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"低阶模型失败准备升级高阶:"
|
||
f"function={function_name}, from_tier=low, to_tier=high, reason={str(error)[:120]}"
|
||
),
|
||
)
|
||
high_engine = await self._resolve_control_plane_high_tier_engine_for_escalation(
|
||
bundle=bundle,
|
||
engines=engines,
|
||
active_engine=active_engine,
|
||
function_name=function_name,
|
||
failed_engine=failed_engine,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
if not isinstance(high_engine, dict):
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶升级未命中,回退通用 high tier:"
|
||
f"function={function_name}, reason={str(error)[:120]}"
|
||
),
|
||
)
|
||
high_engine = self._resolve_tier_engine(
|
||
tier="high",
|
||
engines=engines,
|
||
active_engine=active_engine,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
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",
|
||
)
|
||
forced_high_tier_engine = self._prefer_high_tier_retry_engine(
|
||
bundle=bundle,
|
||
engines=engines,
|
||
function_name=function_name,
|
||
failed_engine=failed_engine,
|
||
current_engine=high_engine,
|
||
)
|
||
if isinstance(forced_high_tier_engine, dict):
|
||
high_engine = forced_high_tier_engine
|
||
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:
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"高阶升级引擎与失败引擎相同,跳过:"
|
||
f"function={function_name}, tier=high"
|
||
),
|
||
)
|
||
return None, None
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"低阶模型失败自动升级:"
|
||
f"function={function_name}, planned_tier=low, executed_tier=high, "
|
||
f"decision=low_to_high_escalated, 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,
|
||
routing_meta=routing_meta,
|
||
)
|
||
text = completion.text
|
||
if not text.strip():
|
||
raise ValueError("high_tier_empty_response")
|
||
if self._call_is_invalid_structured_output_for_retry(
|
||
function_name=function_name,
|
||
response_text=text,
|
||
routing_meta=routing_meta,
|
||
):
|
||
raise ValueError("high_tier_invalid_structured_output")
|
||
high_engine = _merge_engine_routing_meta(
|
||
high_engine,
|
||
tier_alias="low",
|
||
planned_tier="low",
|
||
executed_tier="high",
|
||
routing_decision="low_to_high_escalated",
|
||
)
|
||
high_engine["_provider_request_payload"] = completion.raw_request
|
||
high_engine["_provider_response_payload"] = completion.raw_response
|
||
high_engine["_provider_attempts"] = completion.provider_attempts
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"高阶升级成功:"
|
||
f"function={function_name}, {_routing_meta_log_text(_read_engine_routing_meta(high_engine))}, text_len={len(text)}"
|
||
),
|
||
)
|
||
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,
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> tuple[Optional[StructuredExtractionResult], Optional[dict[str, Any]]]:
|
||
fn = str(function_name or "").strip().lower()
|
||
force_intent_retry = False
|
||
if not self._should_retry_low_tier_with_high_tier(
|
||
function_name=function_name,
|
||
failed_engine=failed_engine,
|
||
routing_meta=routing_meta,
|
||
):
|
||
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) and self._engine_enabled(item)
|
||
]
|
||
active_engine = self._resolve_active_engine_from_bundle(bundle)
|
||
slot_bundle = await self._load_capability_slot_bundle(db)
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"低阶结构化失败准备升级高阶:"
|
||
f"function={function_name}, from_tier=low, to_tier=high, reason={str(error)[:120]}, "
|
||
f"force_intent_retry={force_intent_retry}"
|
||
),
|
||
)
|
||
high_engine = await self._resolve_control_plane_high_tier_engine_for_escalation(
|
||
bundle=bundle,
|
||
engines=engines,
|
||
active_engine=active_engine,
|
||
function_name=function_name,
|
||
failed_engine=failed_engine,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
if not isinstance(high_engine, dict):
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"PMFlow 本地路由层高阶结构化升级未命中,回退通用 high tier:"
|
||
f"function={function_name}, reason={str(error)[:120]}, "
|
||
f"force_intent_retry={force_intent_retry}"
|
||
),
|
||
)
|
||
high_engine = self._resolve_tier_engine(
|
||
tier="high",
|
||
engines=engines,
|
||
active_engine=active_engine,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
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",
|
||
)
|
||
forced_high_tier_engine = self._prefer_high_tier_retry_engine(
|
||
bundle=bundle,
|
||
engines=engines,
|
||
function_name=function_name,
|
||
failed_engine=failed_engine,
|
||
current_engine=high_engine,
|
||
)
|
||
if isinstance(forced_high_tier_engine, dict):
|
||
high_engine = forced_high_tier_engine
|
||
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:
|
||
if not force_intent_retry:
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"高阶结构化升级引擎与失败引擎相同,跳过:"
|
||
f"function={function_name}, tier=high"
|
||
),
|
||
)
|
||
return None, None
|
||
fallback_engine: Optional[dict[str, Any]] = None
|
||
for fallback_tier in ("low",):
|
||
candidate = self._resolve_tier_engine(
|
||
tier=fallback_tier,
|
||
engines=engines,
|
||
active_engine=active_engine,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
if not isinstance(candidate, dict):
|
||
continue
|
||
candidate = self._ensure_engine_allowed_for_function(
|
||
engine=candidate,
|
||
function_name=function_name,
|
||
settings=self._normalize_engine_settings(bundle.get("settings")),
|
||
engine_label=f"intent_structured_failover_{fallback_tier}",
|
||
)
|
||
candidate_provider = str(candidate.get("provider") or "").strip().lower()
|
||
candidate_model = str(candidate.get("model") or "").strip()
|
||
if candidate_provider == failed_provider and candidate_model == failed_model:
|
||
continue
|
||
fallback_engine = candidate
|
||
break
|
||
if fallback_engine is None:
|
||
return None, None
|
||
high_engine = fallback_engine
|
||
high_provider = str(high_engine.get("provider") or "").strip().lower()
|
||
high_model = str(high_engine.get("model") or "").strip()
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"低阶结构化失败自动升级:"
|
||
f"function={function_name}, planned_tier=low, executed_tier=high, "
|
||
f"decision=low_to_high_escalated, reason={str(error)[:120]}, "
|
||
f"force_intent_retry={force_intent_retry}"
|
||
),
|
||
)
|
||
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=(
|
||
"intent_structured_runtime_failover"
|
||
if force_intent_retry
|
||
else function_name
|
||
),
|
||
response_mode=response_mode,
|
||
routing_meta=routing_meta,
|
||
)
|
||
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"
|
||
),
|
||
)
|
||
high_engine = _merge_engine_routing_meta(
|
||
high_engine,
|
||
tier_alias="low",
|
||
planned_tier="low",
|
||
executed_tier="high",
|
||
routing_decision="low_to_high_escalated",
|
||
)
|
||
high_engine["_provider_request_payload"] = completion.raw_request
|
||
high_engine["_provider_response_payload"] = completion.raw_response
|
||
high_engine["_provider_attempts"] = completion.provider_attempts
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"高阶结构化升级成功:"
|
||
f"function={function_name}, {_routing_meta_log_text(_read_engine_routing_meta(high_engine))}, "
|
||
f"structured_source={extraction.source_field or 'none'}"
|
||
),
|
||
)
|
||
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
|
||
|
||
async def _retry_structured_with_slot_fallback_after_primary_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,
|
||
routing_meta: dict[str, Any] | None = None,
|
||
) -> tuple[Optional[StructuredExtractionResult], Optional[dict[str, Any]]]:
|
||
failed_meta = _read_engine_routing_meta(failed_engine)
|
||
if str(failed_meta.get("planned_tier") or "").strip().lower() != "structured":
|
||
return None, None
|
||
try:
|
||
_, bundle = await self._get_or_init_bundle(db, tenant_id=tenant_id)
|
||
settings = self._normalize_engine_settings(bundle.get("settings"))
|
||
engines = [
|
||
item for item in bundle.get("engines", []) if isinstance(item, dict) and self._engine_enabled(item)
|
||
]
|
||
slot_bundle = await self._load_capability_slot_bundle(db)
|
||
slot_version = self._slot_bundle_active_version(slot_bundle)
|
||
failed_engine_id = str(failed_engine.get("id") or "").strip()
|
||
failed_provider = str(failed_engine.get("provider") or "").strip().lower()
|
||
failed_model = str(failed_engine.get("model") or "").strip()
|
||
candidates = self._resolve_engines_from_slot_binding(
|
||
tier="structured",
|
||
engines=engines,
|
||
slot_bundle=slot_bundle,
|
||
)
|
||
for candidate in candidates:
|
||
candidate_id = str(candidate.get("id") or "").strip()
|
||
candidate_provider = str(candidate.get("provider") or "").strip().lower()
|
||
candidate_model = str(candidate.get("model") or "").strip()
|
||
if candidate_id and candidate_id == failed_engine_id:
|
||
continue
|
||
if candidate_provider == failed_provider and candidate_model == failed_model:
|
||
continue
|
||
candidate = self._ensure_engine_allowed_for_function(
|
||
engine=candidate,
|
||
function_name=function_name,
|
||
settings=settings,
|
||
engine_label="structured_slot_fallback",
|
||
)
|
||
candidate = _merge_engine_routing_meta(
|
||
candidate,
|
||
tier_alias="structured",
|
||
planned_tier="structured",
|
||
executed_tier="structured",
|
||
route_source="slot_binding",
|
||
routing_decision="capability_slot_fallback",
|
||
slot_key="structured",
|
||
slot_version=slot_version,
|
||
slot_runtime_source="active_slot",
|
||
slot_fallback_index=candidate.get("_slot_fallback_index", 0),
|
||
slot_degraded=False,
|
||
)
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"结构化槽 fallback 重试:"
|
||
f"function={function_name}, failed={failed_engine_id or failed_provider}, "
|
||
f"fallback={candidate_id or candidate_provider}, reason={str(error)[:120]}"
|
||
),
|
||
)
|
||
completion = await self._chat_completion_raw(
|
||
provider=candidate_provider,
|
||
prompt=prompt,
|
||
system_prompt=system_prompt,
|
||
api_key=str(candidate.get("api_key") or ""),
|
||
base_url=str(candidate.get("base_url") or ""),
|
||
model=candidate_model,
|
||
temperature=temperature,
|
||
max_tokens=max_tokens,
|
||
function_name=function_name,
|
||
response_mode=response_mode,
|
||
routing_meta=_merge_call_routing_meta(
|
||
caller_meta=routing_meta,
|
||
engine_meta=_read_engine_routing_meta(candidate),
|
||
),
|
||
)
|
||
extraction = self._extract_structured_object_from_response(
|
||
function_name=function_name,
|
||
response=completion.raw_response,
|
||
fallback_text=completion.text,
|
||
provider=candidate_provider,
|
||
model=candidate_model,
|
||
)
|
||
if extraction.payload is None:
|
||
_runtime_log(
|
||
"LLM结构化",
|
||
(
|
||
"结构化槽 fallback 返回无效结构:"
|
||
f"function={function_name}, fallback={candidate_id or candidate_provider}, "
|
||
f"error={extraction.error_code or 'structured_schema_invalid'}"
|
||
),
|
||
)
|
||
continue
|
||
candidate["_provider_request_payload"] = completion.raw_request
|
||
candidate["_provider_response_payload"] = completion.raw_response
|
||
candidate["_provider_attempts"] = completion.provider_attempts
|
||
_runtime_log(
|
||
"LLM结构化",
|
||
(
|
||
f"function={function_name}, structured_source={extraction.source_field or 'none'}, "
|
||
"validation=ok, fallback=structured_slot"
|
||
),
|
||
)
|
||
return extraction, candidate
|
||
return None, None
|
||
except Exception as retry_exc:
|
||
_runtime_log(
|
||
"LLM路由",
|
||
(
|
||
"结构化槽 fallback 链失败:"
|
||
f"function={function_name}, error={type(retry_exc).__name__}:{str(retry_exc)[:180]}"
|
||
),
|
||
)
|
||
return None, None
|