266 lines
10 KiB
Python
266 lines
10 KiB
Python
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 不存在")
|
||
|