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", ]