* feat(registry): runtime plugin registration for model backends Add a discovery layer so external packages can contribute model backends to lm-eval without editing its source, and have them behave exactly like upstream backends. The Registry class already supported EntryPoint placeholders, but nothing ever scanned installed distributions for them. Two mechanisms: - Entry points: packages declaring an `lm_eval.models` entry point are discovered automatically (lazy, zero-config) when a model name is resolved. - Explicit import: `--plugins mod` / `LM_EVAL_PLUGINS` env / `simple_evaluate(plugins=...)` import named modules so their `@register_*` decorators run, for local or unpublished components. Also fixes Registry collision handling to upgrade an EntryPoint placeholder to its decorated class (the natural case where a plugin declares both an entry point and `@register_model`), guarding EntryPoint.__eq__ which raises on mismatched types. Co-Authored-By: Claude <[email protected]> * feat(registry): plugin discovery for filters, metrics, and aggregations Extend the entry-point plugin mechanism (added for models) to the other Registry-backed components. External packages can now contribute: - filters via the `lm_eval.filters` entry-point group - metrics via `lm_eval.metrics` - aggregations via `lm_eval.aggregations` get_filter/get_metric/get_aggregation discover installed entry points lazily. Because @register_metric populates the higher_is_better and metric-aggregation registries as a decorator side effect, is_higher_better and get_metric_aggregation now force discovery + materialization of the named metric so plugin metrics carry their metadata even when those getters are called standalone. Generalize the Registry placeholder→concrete upgrade to any object with module/name (functions, not just classes), so metric/aggregation plugins that declare both an entry point and a decorator upgrade cleanly instead of colliding. Co-Authored-By: Claude <[email protected]> * remove plugins from `simple_evaluate` * fix tests * fix docs * add to news --------- Co-authored-by: Baber Abbasi <[email protected]>
4.0 KiB
Plugin Guide
lm-eval resolves components — model backends, filters, metrics, aggregations — by
name against internal registries. A plugin is a component that lives in your own
package and gets registered into those registries at runtime, so lm-eval treats
it exactly like a built-in without you forking or editing the lm-eval source tree.
What can be contributed
| Component | Entry-point group | Decorator | Resolved by |
|---|---|---|---|
| Model | lm_eval.models |
@register_model |
--model <name> |
| Filter | lm_eval.filters |
@register_filter |
filter_list in a task YAML |
| Metric | lm_eval.metrics |
@register_metric |
metric_list in a task YAML |
| Aggregation | lm_eval.aggregations |
@register_aggregation |
aggregation in a task YAML |
Tasks are not plugins — point --include_path at a directory of task YAMLs
instead. See the New Task Guide.
1. Entry points (zero-config, recommended for published packages)
Declare the entry point in your package's pyproject.toml, mapping the name
lm-eval should resolve to module:object:
[project.entry-points."lm_eval.models"]
my-backend = "your_pkg.models:MyBackendLM"
[project.entry-points."lm_eval.metrics"]
my-metric = "your_pkg.metrics:my_metric"
Once your package is pip installed it works with no extra flags:
lm-eval run --model my-backend --tasks hellaswag
Discovery is lazy: the entry point is recorded as a placeholder, and your module is only imported when the component is actually requested. A broken plugin therefore only breaks runs that ask for it, and is reported with the offending name.
Your object can also carry its @register_* decorator — importing the module fires
the decorator, which upgrades the placeholder in place. The two are reconciled
automatically as long as the entry-point value and the decorated object agree.
Metrics need their decorator
higher_is_better and the default aggregation are recorded as side effects of
@register_metric(...), not as separate entry points. So a metric plugin's module
must run the decorator, and the entry point should name the decorated function:
# my_pkg/metrics.py
from lm_eval.api.registry import register_aggregation, register_metric
@register_aggregation("my-agg")
def my_agg(items):
return sum(items) / len(items)
@register_metric(metric="my-metric", higher_is_better=True, aggregation="my-agg")
def my_metric(items):
return sum(items)
If a metric's aggregation is also a plugin, register it in the same module — the
aggregation must already be in the registry when @register_metric runs.
2. Explicit import (for local or unpublished modules)
Point lm-eval at one or more modules to import before evaluation, so their
@register_* decorators run:
lm-eval run --model my-backend --plugins my_pkg.models --tasks hellaswag
--plugins accepts a comma-separated or space-separated list, and is also settable
as plugins: in a --config YAML. A module that fails to import is logged and
skipped rather than aborting the run.
Precedence
Built-in components always win. Discovery skips any alias that is already
registered, so a plugin declaring acc cannot shadow the core acc metric — the
plugin module is not even imported. Pick a distinctive name.
Programmatic use
Entry-point plugins need no wiring at all — they are discovered on first name
resolution, including from lm_eval.simple_evaluate(...).
For a module that is not an installed entry-point plugin, import it yourself before evaluating.
!!! Warning Registration mutates process-global registries and is not scoped to a single evaluation:
import lm_eval
from lm_eval.api.registry import import_plugins
import_plugins(["my_pkg.models"]) # or simply: import my_pkg.models
results = lm_eval.simple_evaluate(model="my-backend", tasks=["hellaswag"])