239 lines
8.6 KiB
Python
239 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
DEFAULT_ROUTING_RULES_RELATIVE_PATH = "../.openclaw/pmflow-routing-rules.json"
|
|
_APP_ROOT = Path(__file__).resolve().parents[2]
|
|
_DEFAULT_ROUTING_RULES_PATH = (_APP_ROOT / DEFAULT_ROUTING_RULES_RELATIVE_PATH).resolve()
|
|
|
|
_PROVIDER_ALIASES = {
|
|
"zhipu": "glm",
|
|
"chatglm": "glm",
|
|
"deep-seek": "deepseek",
|
|
"local": "ollama",
|
|
}
|
|
|
|
_DEFAULT_PROVIDER_DEFAULTS: dict[str, dict[str, str]] = {
|
|
"deepseek": {
|
|
"name": "DeepSeek",
|
|
"base_url": "https://api.deepseek.com/v1",
|
|
"model": "deepseek-chat",
|
|
"api_env": "DEEPSEEK_API_KEY",
|
|
},
|
|
"glm": {
|
|
"name": "Qwen (DashScope)",
|
|
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
"model": "qwen-plus",
|
|
"api_env": "DASHSCOPE_API_KEY",
|
|
},
|
|
"openai": {
|
|
"name": "OpenAI",
|
|
"base_url": "https://api.edgefn.net/v1",
|
|
"model": "MiniMax-M2.5",
|
|
"api_env": "OPENAI_API_KEY",
|
|
},
|
|
"ollama": {
|
|
"name": "Ollama",
|
|
"base_url": "http://127.0.0.1:11434",
|
|
"model": "llama3:latest",
|
|
"api_env": "",
|
|
},
|
|
}
|
|
|
|
_DEFAULT_TIER_DEFAULTS: dict[str, dict[str, str]] = {
|
|
"low": {
|
|
"provider": "glm",
|
|
"model": "qwen-plus",
|
|
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
"api_env": "DASHSCOPE_API_KEY",
|
|
},
|
|
"high": {
|
|
"provider": "openai",
|
|
"model": "GLM-5",
|
|
"base_url": "https://api.edgefn.net/v1",
|
|
"api_env": "OPENAI_API_KEY",
|
|
},
|
|
"safety": {
|
|
"provider": "glm",
|
|
"model": "qwen-plus",
|
|
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
"api_env": "DASHSCOPE_API_KEY",
|
|
},
|
|
}
|
|
|
|
_CONFIG_CACHE: dict[str, Any] = {"path": "", "mtime": -1.0, "bundle": None}
|
|
|
|
|
|
def _normalize_provider(provider: str) -> str:
|
|
value = str(provider or "").strip().lower()
|
|
value = _PROVIDER_ALIASES.get(value, value)
|
|
if value not in _DEFAULT_PROVIDER_DEFAULTS:
|
|
raise ValueError("provider 仅支持 deepseek/glm/openai/ollama")
|
|
return value
|
|
|
|
|
|
def _normalize_tier(tier: str) -> str:
|
|
value = str(tier or "").strip().lower()
|
|
if value not in _DEFAULT_TIER_DEFAULTS:
|
|
raise ValueError("tier 仅支持 low/high/safety")
|
|
return value
|
|
|
|
|
|
def resolve_model_config_path(path: str | Path | None = None) -> Path:
|
|
raw = path if path is not None else os.getenv("OPENCLAW_ROUTING_RULES_PATH", "")
|
|
raw_text = str(raw or "").strip()
|
|
if not raw_text:
|
|
return _DEFAULT_ROUTING_RULES_PATH
|
|
candidate = Path(raw_text).expanduser()
|
|
if candidate.is_absolute():
|
|
return candidate
|
|
return (_APP_ROOT / candidate).resolve()
|
|
|
|
|
|
def _normalize_provider_defaults(raw: Any) -> dict[str, dict[str, str]]:
|
|
payload = raw if isinstance(raw, dict) else {}
|
|
result: dict[str, dict[str, str]] = {}
|
|
for provider, defaults in _DEFAULT_PROVIDER_DEFAULTS.items():
|
|
item = payload.get(provider) if isinstance(payload.get(provider), dict) else {}
|
|
result[provider] = {
|
|
"name": str(item.get("name") or defaults["name"]).strip(),
|
|
"base_url": str(item.get("base_url") or defaults["base_url"]).strip().rstrip("/"),
|
|
"model": str(item.get("model") or defaults["model"]).strip(),
|
|
"api_env": str(item.get("api_env") or defaults["api_env"]).strip(),
|
|
}
|
|
return result
|
|
|
|
|
|
def _normalize_tier_defaults(raw: Any) -> dict[str, dict[str, str]]:
|
|
payload = raw if isinstance(raw, dict) else {}
|
|
result: dict[str, dict[str, str]] = {}
|
|
for tier, defaults in _DEFAULT_TIER_DEFAULTS.items():
|
|
item = payload.get(tier) if isinstance(payload.get(tier), dict) else {}
|
|
provider_raw = str(item.get("provider") or defaults["provider"]).strip()
|
|
provider = _normalize_provider(provider_raw)
|
|
result[tier] = {
|
|
"provider": provider,
|
|
"model": str(item.get("model") or defaults["model"]).strip(),
|
|
"base_url": str(item.get("base_url") or defaults["base_url"]).strip().rstrip("/"),
|
|
"api_env": str(item.get("api_env") or defaults["api_env"]).strip(),
|
|
}
|
|
return result
|
|
|
|
|
|
def _normalize_bundle(raw: Any, *, config_path: Path) -> dict[str, Any]:
|
|
payload = raw if isinstance(raw, dict) else {}
|
|
bundle = {
|
|
"version": str(payload.get("version") or "v3").strip(),
|
|
"updated_at": str(payload.get("updated_at") or "").strip(),
|
|
"source_of_truth": str(payload.get("source_of_truth") or "openclaw").strip(),
|
|
"description": str(payload.get("description") or "").strip(),
|
|
"config_path": str(config_path),
|
|
"provider_defaults": _normalize_provider_defaults(payload.get("provider_defaults")),
|
|
"tier_defaults": _normalize_tier_defaults(payload.get("tier_defaults")),
|
|
"rules": [deepcopy(item) for item in payload.get("rules", []) if isinstance(item, dict)],
|
|
}
|
|
return bundle
|
|
|
|
|
|
def _load_raw_payload(config_path: Path) -> dict[str, Any]:
|
|
if not config_path.exists():
|
|
return {}
|
|
try:
|
|
parsed = json.loads(config_path.read_text(encoding="utf-8"))
|
|
return parsed if isinstance(parsed, dict) else {}
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def load_model_config(*, refresh: bool = False, path: str | Path | None = None) -> dict[str, Any]:
|
|
config_path = resolve_model_config_path(path)
|
|
mtime = config_path.stat().st_mtime if config_path.exists() else -1.0
|
|
cached = _CONFIG_CACHE
|
|
if (
|
|
not refresh
|
|
and cached.get("bundle") is not None
|
|
and cached.get("path") == str(config_path)
|
|
and cached.get("mtime") == mtime
|
|
):
|
|
return deepcopy(cached["bundle"])
|
|
|
|
raw = _load_raw_payload(config_path)
|
|
bundle = _normalize_bundle(raw, config_path=config_path)
|
|
_CONFIG_CACHE.update({"path": str(config_path), "mtime": mtime, "bundle": deepcopy(bundle)})
|
|
return deepcopy(bundle)
|
|
|
|
|
|
def get_provider_defaults(provider: str | None = None, *, refresh: bool = False) -> dict[str, Any]:
|
|
bundle = load_model_config(refresh=refresh)
|
|
defaults = bundle.get("provider_defaults") if isinstance(bundle.get("provider_defaults"), dict) else {}
|
|
if provider is None:
|
|
return deepcopy(defaults)
|
|
provider_name = _normalize_provider(provider)
|
|
fallback = _DEFAULT_PROVIDER_DEFAULTS[provider_name]
|
|
return deepcopy(defaults.get(provider_name, fallback))
|
|
|
|
|
|
def get_tier_defaults(tier: str | None = None, *, refresh: bool = False) -> dict[str, Any]:
|
|
bundle = load_model_config(refresh=refresh)
|
|
defaults = bundle.get("tier_defaults") if isinstance(bundle.get("tier_defaults"), dict) else {}
|
|
if tier is None:
|
|
return deepcopy(defaults)
|
|
tier_name = _normalize_tier(tier)
|
|
fallback = _DEFAULT_TIER_DEFAULTS[tier_name]
|
|
return deepcopy(defaults.get(tier_name, fallback))
|
|
|
|
|
|
def build_provider_runtime(
|
|
provider: str,
|
|
*,
|
|
api_key: str = "",
|
|
model: str | None = None,
|
|
base_url: str | None = None,
|
|
) -> dict[str, Any]:
|
|
provider_name = _normalize_provider(provider)
|
|
defaults = get_provider_defaults(provider_name)
|
|
return {
|
|
"id": f"env-{provider_name}",
|
|
"name": str(defaults.get("name") or _DEFAULT_PROVIDER_DEFAULTS[provider_name]["name"]).strip(),
|
|
"provider": provider_name,
|
|
"api_key": str(api_key or "").strip(),
|
|
"base_url": str(base_url or defaults.get("base_url") or "").strip().rstrip("/"),
|
|
"model": str(model or defaults.get("model") or "").strip(),
|
|
"api_env": str(defaults.get("api_env") or _DEFAULT_PROVIDER_DEFAULTS[provider_name]["api_env"]).strip(),
|
|
}
|
|
|
|
|
|
def build_tier_runtime(
|
|
tier: str,
|
|
*,
|
|
api_key: str = "",
|
|
) -> dict[str, Any]:
|
|
tier_name = _normalize_tier(tier)
|
|
tier_defaults = get_tier_defaults(tier_name)
|
|
provider_name = _normalize_provider(str(tier_defaults.get("provider") or "glm"))
|
|
provider_defaults = get_provider_defaults(provider_name)
|
|
return {
|
|
"id": f"tier-{tier_name}",
|
|
"name": str(provider_defaults.get("name") or "").strip(),
|
|
"provider": provider_name,
|
|
"api_key": str(api_key or "").strip(),
|
|
"base_url": str(tier_defaults.get("base_url") or provider_defaults.get("base_url") or "").strip().rstrip("/"),
|
|
"model": str(tier_defaults.get("model") or provider_defaults.get("model") or "").strip(),
|
|
"api_env": str(tier_defaults.get("api_env") or provider_defaults.get("api_env") or "").strip(),
|
|
"tier": tier_name,
|
|
}
|
|
|
|
|
|
__all__ = [
|
|
"build_provider_runtime",
|
|
"build_tier_runtime",
|
|
"get_provider_defaults",
|
|
"get_tier_defaults",
|
|
"load_model_config",
|
|
"resolve_model_config_path",
|
|
]
|