PMFlow/backend/app/services/model/model_config.py

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