mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): register classifier plugins before the model list resolves, clear the registry when the config drops the block
A config-file auto-router referencing a registry name resolved against an empty registry because registration ran after the model list, failing startup as a dotted import of the name. Registration now precedes the model list, and the replace-on-reload semantics also cover an emptied or removed classifier_plugins block, so stale names cannot stay selectable
This commit is contained in:
parent
0c61bee7ed
commit
9a3e784d23
2 changed files with 83 additions and 25 deletions
|
|
@ -5311,6 +5311,32 @@ class ProxyConfig:
|
|||
router_params["health_check_staleness_threshold"] = _hc_staleness
|
||||
if _hc_ignore_transient:
|
||||
router_params["health_check_ignore_transient_errors"] = True
|
||||
## CLASSIFIER PLUGINS (complexity-router custom classifiers, picked by name in the Admin UI).
|
||||
## Registered before the model list resolves, so config-file routers can reference names.
|
||||
classifier_plugins_config: Final = config.get("classifier_plugins", None)
|
||||
if classifier_plugins_config is not None and not isinstance(classifier_plugins_config, dict):
|
||||
raise TypeError("classifier_plugins must map plugin names to dotted paths")
|
||||
for plugin_name, plugin_path in (classifier_plugins_config or _EMPTY_MAPPING).items():
|
||||
if not isinstance(plugin_path, str):
|
||||
raise TypeError(f"classifier_plugins.{plugin_name} must be a dotted-path string")
|
||||
resolved_classifier_entries: Final = tuple(
|
||||
(
|
||||
str(plugin_name),
|
||||
resolve_classifier_plugin(
|
||||
plugin_path=plugin_path,
|
||||
config_file_path=config_file_path,
|
||||
source_label=f"classifier_plugins.{plugin_name}",
|
||||
),
|
||||
)
|
||||
for plugin_name, plugin_path in (classifier_plugins_config or _EMPTY_MAPPING).items()
|
||||
)
|
||||
# Replace, never merge: a reload that drops a name, empties the block, or removes it
|
||||
# entirely must evict the stale entries, or a deleted plugin stays selectable until
|
||||
# the next restart. Resolution runs before the clear, so a module broken at reload
|
||||
# time keeps the old registry intact.
|
||||
litellm.classifier_plugin_registry.clear()
|
||||
litellm.classifier_plugin_registry.update(resolved_classifier_entries)
|
||||
|
||||
## MODEL LIST
|
||||
model_list: Final = config.get("model_list", None)
|
||||
if model_list:
|
||||
|
|
@ -5492,31 +5518,6 @@ class ProxyConfig:
|
|||
# Load vector stores from config
|
||||
litellm.vector_store_registry.load_vector_stores_from_config(vector_store_registry_config)
|
||||
|
||||
## CLASSIFIER PLUGINS (complexity-router custom classifiers, picked by name in the Admin UI)
|
||||
classifier_plugins_config: Final = config.get("classifier_plugins", None)
|
||||
if classifier_plugins_config:
|
||||
if not isinstance(classifier_plugins_config, dict):
|
||||
raise TypeError("classifier_plugins must map plugin names to dotted paths")
|
||||
for plugin_name, plugin_path in classifier_plugins_config.items():
|
||||
if not isinstance(plugin_path, str):
|
||||
raise TypeError(f"classifier_plugins.{plugin_name} must be a dotted-path string")
|
||||
resolved_entries: Final = tuple(
|
||||
(
|
||||
str(plugin_name),
|
||||
resolve_classifier_plugin(
|
||||
plugin_path=plugin_path,
|
||||
config_file_path=config_file_path,
|
||||
source_label=f"classifier_plugins.{plugin_name}",
|
||||
),
|
||||
)
|
||||
for plugin_name, plugin_path in classifier_plugins_config.items()
|
||||
)
|
||||
# Replace, never merge: a config reload that drops a name must evict it, or a
|
||||
# deleted plugin stays selectable until the next restart. Resolution runs before
|
||||
# the clear, so a module broken at reload time keeps the old registry intact.
|
||||
litellm.classifier_plugin_registry.clear()
|
||||
litellm.classifier_plugin_registry.update(resolved_entries)
|
||||
|
||||
## WORKER REGISTRY (Global Control Plane)
|
||||
worker_registry_config: Final = config.get("worker_registry", None)
|
||||
if worker_registry_config:
|
||||
|
|
|
|||
|
|
@ -297,6 +297,63 @@ def test_classifier_plugins_config_key_replaces_the_registry_on_reload(monkeypat
|
|||
assert set(litellm.classifier_plugin_registry) == {"fresh-name"}
|
||||
|
||||
|
||||
def test_config_file_router_can_reference_a_registry_name(monkeypatch, tmp_path):
|
||||
"""The registry registers before the model list resolves, so a config.yaml router may
|
||||
set classifier_plugin to a registry name; regression for the ordering bug where the
|
||||
registry filled after plugin resolution and names failed as dotted imports."""
|
||||
import asyncio
|
||||
|
||||
import litellm
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
(tmp_path / "reg_classifier.py").write_text(
|
||||
"class _Classifier:\n"
|
||||
" async def classify(self, context):\n"
|
||||
" return 'SIMPLE'\n"
|
||||
"\n"
|
||||
"instance = _Classifier()\n"
|
||||
)
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
"classifier_plugins:\n"
|
||||
" tier-by-team: reg_classifier.instance\n"
|
||||
"model_list:\n"
|
||||
" - model_name: smart-router\n"
|
||||
" litellm_params:\n"
|
||||
" model: auto_router/complexity_router\n"
|
||||
" complexity_router_default_model: gpt-4o-mini\n"
|
||||
" complexity_router_config:\n"
|
||||
" classifier_type: custom\n"
|
||||
" classifier_plugin: tier-by-team\n"
|
||||
" tiers:\n"
|
||||
" SIMPLE: gpt-4o-mini\n"
|
||||
)
|
||||
monkeypatch.setattr(litellm, "classifier_plugin_registry", {}, raising=True)
|
||||
proxy_config = ProxyConfig()
|
||||
router, _, _ = asyncio.run(proxy_config.load_config(router=None, config_file_path=str(config_path)))
|
||||
assert "smart-router" in router.model_names
|
||||
assert list(router.complexity_routers) == ["smart-router"]
|
||||
|
||||
|
||||
def test_registry_clears_when_the_config_key_is_removed(monkeypatch, tmp_path):
|
||||
"""Removing the classifier_plugins block on reload must evict every stale entry."""
|
||||
import asyncio
|
||||
|
||||
import litellm
|
||||
from litellm.proxy.proxy_server import ProxyConfig
|
||||
|
||||
class _Classifier:
|
||||
async def classify(self, context):
|
||||
return "SIMPLE"
|
||||
|
||||
monkeypatch.setattr(litellm, "classifier_plugin_registry", {"stale-name": _Classifier()}, raising=True)
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text("model_list:\n - model_name: gpt-4o-mini\n litellm_params:\n model: gpt-4o-mini\n")
|
||||
proxy_config = ProxyConfig()
|
||||
asyncio.run(proxy_config.load_config(router=None, config_file_path=str(config_path)))
|
||||
assert litellm.classifier_plugin_registry == {}
|
||||
|
||||
|
||||
def test_resolve_complexity_router_plugins_leaves_live_classifier_instance_alone():
|
||||
class _Classifier:
|
||||
async def classify(self, context):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue