refactor(proxy): swap the classifier registry atomically on reload, rename the dual-form resolver parameter

Rebinding the module attribute replaces clear-then-update, so an in-flight
request validating mid-reload sees the old set or the new one and never an
empty window, matching how vector_store_registry swaps. plugin_path becomes
plugin_reference since the resolver accepts a registry name or a dotted path.
Also drops a leftover no-op line in the reload test
This commit is contained in:
Tin Chi Lo 2026-08-18 17:59:04 -07:00
parent 9a3e784d23
commit 95f6653698
2 changed files with 11 additions and 12 deletions

View file

@ -4049,7 +4049,7 @@ def resolve_complexity_router_plugins(
classifier_plugin_path: Final = complexity_router_config.get("classifier_plugin")
if isinstance(classifier_plugin_path, str):
resolved_classifier: Final = resolve_classifier_plugin(
plugin_path=classifier_plugin_path,
plugin_reference=classifier_plugin_path,
config_file_path=config_file_path,
source_label=f"complexity_router_config.classifier_plugin on model {model_name!r}",
)
@ -4079,7 +4079,7 @@ def pin_complexity_router_model_id(model: dict) -> None: # mutable-ok: out-para
def resolve_classifier_plugin(
plugin_path: str,
plugin_reference: str,
config_file_path: str | None,
source_label: str,
) -> ClassifierPlugin:
@ -4089,15 +4089,15 @@ def resolve_classifier_plugin(
sync `def classify` passes the runtime_checkable isinstance and would only fail on the
first classified request, so reject it here where the error names the config key.
"""
registered: Final = litellm.classifier_plugin_registry.get(plugin_path)
registered: Final = litellm.classifier_plugin_registry.get(plugin_reference)
if registered is not None:
return registered
resolved: Final = get_instance_fn(value=plugin_path, config_file_path=config_file_path)
resolved: Final = get_instance_fn(value=plugin_reference, config_file_path=config_file_path)
if not isinstance(resolved, ClassifierPlugin) or not inspect.iscoroutinefunction(
getattr(resolved, "classify", None)
):
raise ValueError(
f"{source_label} entry {plugin_path!r} resolved to {resolved!r}, which does not "
f"{source_label} entry {plugin_reference!r} resolved to {resolved!r}, which does not "
"implement the ClassifierPlugin interface (an async `classify(context)` method). Fix "
"the referenced module before starting the proxy."
)
@ -5323,7 +5323,7 @@ class ProxyConfig:
(
str(plugin_name),
resolve_classifier_plugin(
plugin_path=plugin_path,
plugin_reference=plugin_path,
config_file_path=config_file_path,
source_label=f"classifier_plugins.{plugin_name}",
),
@ -5332,10 +5332,10 @@ class ProxyConfig:
)
# 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)
# the next restart. Rebinding swaps the mapping atomically, so an in-flight request
# validating mid-reload sees the old set or the new one, never an empty window, and
# a module broken at reload time keeps the old registry intact.
litellm.classifier_plugin_registry = dict(resolved_classifier_entries) # mutable-ok: the registry global's type
## MODEL LIST
model_list: Final = config.get("model_list", None)

View file

@ -251,7 +251,7 @@ def test_resolve_classifier_plugin_prefers_the_registry_over_dotted_import(monke
instance = _Classifier()
monkeypatch.setitem(litellm.classifier_plugin_registry, "tier-by-team", instance)
resolved = resolve_classifier_plugin(
plugin_path="tier-by-team", config_file_path=None, source_label="classifier_plugins.tier-by-team"
plugin_reference="tier-by-team", config_file_path=None, source_label="classifier_plugins.tier-by-team"
)
assert resolved is instance
@ -280,7 +280,6 @@ def test_classifier_plugins_config_key_replaces_the_registry_on_reload(monkeypat
" stale-name: reg_classifier.instance\n"
)
monkeypatch.setattr(litellm, "classifier_plugin_registry", {}, raising=True)
asyncio.get_event_loop_policy()
proxy_config = ProxyConfig()
asyncio.run(proxy_config.load_config(router=None, config_file_path=str(config_path)))
assert set(litellm.classifier_plugin_registry) == {"stale-name"}