from __future__ import annotations from .shared import * class LLMTierEngineSelectionMixin: _PROTECTED_STRUCTURED_FUNCTIONS = frozenset( { "pmflow_launch_draft_v1", "pmflow_requirement_plan_draft", "pmflow_requirement_plan_round", "project_requirement_extraction", "generate_task_breakdown", "decompose_tasks_from_charter", "sstc_ai_breakdown", "pmflow_os_changeset_extraction", "pmflow_change_set_extraction", "project_changeset_extraction", } ) @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) and self._engine_enabled(e) ] routed = self._resolve_tier_engine( tier="high", engines=engines, active_engine=active_engine, slot_bundle=await self._load_capability_slot_bundle(db), ) 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 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}, decision=tier_fallback_engine" ), ) return candidate return None def _resolve_tier_engine( self, *, tier: str, engines: list[dict[str, Any]], active_engine: dict[str, Any], slot_bundle: dict[str, Any] | None = None, ) -> Optional[dict[str, Any]]: tier = str(tier or "").strip().lower() if tier not in {"low", "high"}: return None slot_routed = self._resolve_engine_from_slot_binding( tier=tier, engines=engines, slot_bundle=slot_bundle, ) if slot_routed is not None: return self._ensure_control_plane_engine( _merge_engine_routing_meta( slot_routed, tier_alias=tier, planned_tier=tier, executed_tier=tier, route_source="slot_binding", routing_decision="capability_slot", ) ) tier_defaults = dict(TIER_DEFAULTS.get(tier) or {}) provider_env = str(tier_defaults.get("provider") or "dashscope").strip() routed: Optional[dict[str, Any]] = None 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: return None provider = str(routed.get("provider") or "").strip().lower() if tier_defaults.get("model"): routed["model"] = str(tier_defaults.get("model") or routed.get("model") or "").strip() if tier_defaults.get("base_url"): routed["base_url"] = str(tier_defaults.get("base_url") or routed.get("base_url") or "").strip().rstrip("/") routed = _merge_engine_routing_meta( routed, tier_alias=tier, planned_tier=tier, executed_tier=tier, route_source="default", routing_decision="tier_default", ) if str(routed.get("id") or "") != str(active_engine.get("id") or ""): _runtime_log( "LLM路由", ( "混合路由已命中:" f"tier={tier}, decision=tier_default" ), ) return self._ensure_control_plane_engine(routed) def _resolve_capability_slot_engine( self, *, slot_key: str, engines: list[dict[str, Any]], slot_bundle: dict[str, Any] | None, ) -> Optional[dict[str, Any]]: slot_key = str(slot_key or "").strip().lower() if slot_key not in CAPABILITY_SLOT_ALIASES: return None routed = self._resolve_engine_from_slot_binding( tier=slot_key, engines=engines, slot_bundle=slot_bundle, ) if routed is None: return None return self._ensure_control_plane_engine( _merge_engine_routing_meta( routed, tier_alias=slot_key, planned_tier=slot_key, executed_tier=slot_key, route_source="slot_binding", routing_decision="capability_slot", slot_key=slot_key, slot_version=self._slot_bundle_active_version(slot_bundle), slot_runtime_source="active_slot", slot_fallback_index=routed.get("_slot_fallback_index", 0), slot_degraded=False, ) ) def _resolve_structured_slot_engine( self, *, routing_meta: dict[str, Any] | None, engines: list[dict[str, Any]], slot_bundle: dict[str, Any] | None, ) -> Optional[dict[str, Any]]: meta = _normalize_routing_meta(routing_meta) if str(meta.get("response_contract") or "").strip().lower() != "structured": return None return self._resolve_capability_slot_engine( slot_key="structured", engines=engines, slot_bundle=slot_bundle, ) @staticmethod def _requested_runtime_capability_slot(routing_meta: dict[str, Any] | None) -> str: meta = _normalize_routing_meta(routing_meta) if str(meta.get("response_contract") or "").strip().lower() == "structured": return "structured" explicit_slot = _optional_tier_alias( meta.get("slot_key") or meta.get("planned_tier") or meta.get("tier_alias") or meta.get("executed_tier") ) if explicit_slot in {"long_context", "tool"}: return explicit_slot execution_mode = str(meta.get("execution_mode") or "").strip().lower() if execution_mode in {"local_planner", "local_worker"}: return "tool" return "" def _resolve_requested_capability_slot_engine( self, *, routing_meta: dict[str, Any] | None, engines: list[dict[str, Any]], slot_bundle: dict[str, Any] | None, ) -> Optional[dict[str, Any]]: slot_key = self._requested_runtime_capability_slot(routing_meta) if slot_key not in {"structured", "long_context", "tool"}: return None return self._resolve_capability_slot_engine( slot_key=slot_key, engines=engines, slot_bundle=slot_bundle, ) def _resolve_engine_from_slot_binding( self, *, tier: str, engines: list[dict[str, Any]], slot_bundle: dict[str, Any] | None, ) -> Optional[dict[str, Any]]: candidates = self._resolve_engines_from_slot_binding( tier=tier, engines=engines, slot_bundle=slot_bundle, ) return candidates[0] if candidates else None @staticmethod def _active_slot_map(slot_bundle: dict[str, Any] | None) -> dict[str, Any]: if not isinstance(slot_bundle, dict): return {} active = slot_bundle.get("active") if isinstance(slot_bundle.get("active"), dict) else {} if isinstance(active.get("slots"), dict): return active["slots"] if isinstance(slot_bundle.get("slots"), dict): return slot_bundle["slots"] return {} @staticmethod def _slot_bundle_active_version(slot_bundle: dict[str, Any] | None) -> str: if not isinstance(slot_bundle, dict): return "" return str(slot_bundle.get("active_version") or slot_bundle.get("version") or "").strip() def _slot_binding_configured( self, *, slot_bundle: dict[str, Any] | None, tier: str, ) -> bool: slot = self._active_slot_map(slot_bundle).get(str(tier or "").strip().lower()) return isinstance(slot, dict) and bool(str(slot.get("primary_engine_id") or "").strip()) def _engine_satisfies_slot_runtime_requirements( self, *, tier: str, engine: dict[str, Any], ) -> bool: normalized_tier = str(tier or "").strip().lower() if normalized_tier not in {"structured", "long_context", "tool"}: return True capabilities = self._normalize_engine_capabilities( engine.get("capabilities"), provider=str(engine.get("provider") or ""), model=str(engine.get("model") or ""), ) if normalized_tier == "structured": return bool(capabilities.get("structured_output")) and bool(capabilities.get("json_schema")) if normalized_tier == "tool": return bool(capabilities.get("tool_calling")) context_tokens = capabilities.get("max_context_tokens") try: max_context_tokens = int(context_tokens) if context_tokens is not None else 0 except (TypeError, ValueError): max_context_tokens = 0 return bool(capabilities.get("long_context")) and max_context_tokens >= 32768 def _resolve_engines_from_slot_binding( self, *, tier: str, engines: list[dict[str, Any]], slot_bundle: dict[str, Any] | None, ) -> list[dict[str, Any]]: tier = str(tier or "").strip().lower() slots = self._active_slot_map(slot_bundle) slot = slots.get(tier) if not isinstance(slot, dict): return [] ordered_ids = [str(slot.get("primary_engine_id") or "").strip()] if isinstance(slot.get("fallback_engine_ids"), list): ordered_ids.extend(str(item or "").strip() for item in slot.get("fallback_engine_ids", [])) seen: set[str] = set() candidates: list[dict[str, Any]] = [] for fallback_index, engine_id in enumerate(ordered_ids): if not engine_id or engine_id in seen: continue seen.add(engine_id) for item in engines: if str(item.get("id") or "").strip() != engine_id: continue candidate = self._normalize_engine_runtime(item) provider = str(candidate.get("provider") or "").strip().lower() if provider != "ollama" and not str(candidate.get("api_key") or "").strip(): continue if not self._engine_satisfies_slot_runtime_requirements(tier=tier, engine=candidate): required = { "structured": "structured_output/json_schema", "long_context": "long_context/max_context_tokens>=32768", "tool": "tool_calling", }.get(tier, "slot_capability") _runtime_log( "LLM路由", ( "能力槽运行时因能力不足被跳过:" f"slot={tier}, engine={engine_id}, required={required}" ), ) continue candidate["_slot_fallback_index"] = fallback_index candidates.append(candidate) break return candidates 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 不存在")