feat(providers): Allow plugins to add and override model providers
linuztx committed
Mar 12, 2026 at 08:29 UTC
342a9584cc0b5ea4f852513b8e702b05bf5ae240
2 files changed
+55
-22
helpers/plugins.py
+2
@@ -79,6 +79,8 @@ class PluginListItem(BaseModel):
79
80
def after_plugin_change(plugin_names: list[str] | None = None):
81
clear_plugin_cache()
82
+ from helpers.providers import reload_providers
83
+ reload_providers()
84
send_frontend_reload_notification(plugin_names)
85
86
helpers/providers.py
+53
-22
@@ -20,39 +20,65 @@ class ProviderManager:
20
cls._instance = cls()
21
return cls._instance
22
23
+ @classmethod
24
+ def reload(cls):
25
+ """Force reload of all provider configs (call after plugin changes)."""
26
+ inst = cls.get_instance()
27
+ inst._load_providers()
28
+
29
def __init__(self):
30
if self._raw is None or self._options is None:
31
self._load_providers()
32
27
- def _load_providers(self):
28
- """Loads provider configurations from the YAML file and normalises them."""
33
+ @staticmethod
34
+ def _load_yaml(path: str) -> dict:
35
try:
30
- config_path = files.get_abs_path("conf/model_providers.yaml")
31
- with open(config_path, "r", encoding="utf-8") as f:
32
- raw_yaml = yaml.safe_load(f) or {}
36
+ with open(path, "r", encoding="utf-8") as f:
37
+ return yaml.safe_load(f) or {}
38
except (FileNotFoundError, yaml.YAMLError):
34
- raw_yaml = {}
35
-
36
- # ------------------------------------------------------------
37
- # Normalise the YAML so that internally we always work with a
38
- # list-of-dicts [{id, name, ...}] for each provider type. This
39
- # keeps existing callers unchanged while allowing the new nested
40
- # mapping format in the YAML (id -> { ... }).
41
- # ------------------------------------------------------------
42
- normalised: Dict[str, List[Dict[str, str]]] = {}
39
+ return {}
40
41
+ @staticmethod
42
+ def _normalise_yaml(raw_yaml: dict) -> Dict[str, Dict[str, Dict[str, str]]]:
43
+ """Normalise YAML into {type: {id: config}} mapping format."""
44
+ result: Dict[str, Dict[str, Dict[str, str]]] = {}
45
for p_type, providers in (raw_yaml or {}).items():
45
- items: List[Dict[str, str]] = []
46
-
46
+ entries: Dict[str, Dict[str, str]] = {}
47
if isinstance(providers, dict):
48
- # New format: mapping of id -> config
48
for pid, cfg in providers.items():
50
- entry = {"id": pid, **(cfg or {})}
51
- items.append(entry)
49
+ entries[pid] = cfg or {}
50
elif isinstance(providers, list):
53
- # Legacy list format – use as-is
54
- items.extend(providers or [])
51
+ for p in (providers or []):
52
+ pid = (p.get("id") or p.get("value") or "").lower()
53
+ if pid:
54
+ entries[pid] = {k: v for k, v in p.items() if k not in ("id", "value")}
55
+ result[p_type] = entries
56
+ return result
57
58
+ def _load_providers(self):
59
+ """Loads provider configs from main YAML and enabled plugins, then merges."""
60
+ # Load base config
61
+ base_path = files.get_abs_path("conf/model_providers.yaml")
62
+ merged = self._normalise_yaml(self._load_yaml(base_path))
63
+
64
+ # Merge plugin provider configs (enabled plugins only)
65
+ from helpers.plugins import get_enabled_plugin_paths
66
+ plugin_yamls = get_enabled_plugin_paths(None, "conf", "model_providers.yaml")
67
+ for plugin_yaml_path in plugin_yamls:
68
+ plugin_data = self._normalise_yaml(self._load_yaml(plugin_yaml_path))
69
+ for p_type, providers in plugin_data.items():
70
+ if p_type not in merged:
71
+ merged[p_type] = {}
72
+ # Overwrite matching keys, append new ones
73
+ merged[p_type].update(providers)
74
+
75
+ # Convert merged {type: {id: config}} to normalised list format
76
+ normalised: Dict[str, List[Dict[str, str]]] = {}
77
+ for p_type, providers in merged.items():
78
+ items: List[Dict[str, str]] = []
79
+ for pid, cfg in providers.items():
80
+ entry = {"id": pid, **cfg}
81
+ items.append(entry)
82
normalised[p_type] = items
83
84
# Save raw
@@ -98,4 +124,9 @@ def get_raw_providers(provider_type: ModelType) -> List[Dict[str, str]]:
124
125
def get_provider_config(provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]:
126
"""Return metadata for a single provider (None if not found)."""
101
- return ProviderManager.get_instance().get_provider_config(provider_type, provider_id)
\ No newline at end of file
127
+ return ProviderManager.get_instance().get_provider_config(provider_type, provider_id)
128
+
129
+
130
+def reload_providers():
131
+ """Re-merge base + plugin provider configs. Call after plugin changes."""
132
+ ProviderManager.reload()
\ No newline at end of file