Add ssm_configs: model config schemas, registries, and a plugin system - #354
Add ssm_configs: model config schemas, registries, and a plugin system#354digicosmos86 wants to merge 23 commits into
Conversation
Move ssms/ssm_configs to a top-level ssm_configs distribution with a src layout and its own pyproject (uv build backend). The import path changes from ssms.ssm_configs to ssm_configs; nothing imported it yet, so no call sites need updating. pydantic is now declared as a dependency of ssm-configs itself rather than of the parent package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
Declare ssm_configs as a workspace member and add ssm-configs to the dev dependency group only, so it is installed editable into the venv for development without becoming a runtime dependency of ssm-simulators. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
pydantic is now declared by the ssm-configs package, so the temporary optional dependency group on the parent package is no longer needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
__init__subclass__ was a typo for __init_subclass__, so the hook never ran and every subclass definition raised TypeError at import time; prefix and base_path were consequently never set. Also declare prefix, base_path, internal_models and external_models, and type model_schema as type[C] rather than C (subclasses assign the schema class, not an instance), which clears the remaining mypy errors, plus a missing blank line in schema.py flagged by ruff format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
Add a per-subclass __new__ so constructing a registry always returns the same object, which makes the existing initialized guard in __init__ do what it claimed: cls.__dict__ is consulted rather than cls._instance so a subclass cannot inherit the base class's instance. Instantiate hssm_registry and rlssm_registry at module scope and export them, along with the registry and schema classes, from the package __init__. external_models moves from a base-class dict shared by every registry to one created per subclass in __init_subclass__, so an external model registered on one registry no longer shows up in the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
Drop BaseModelRegistry, HSSMRegistry and RLSSMRegistry from the package namespace so hssm_registry and rlssm_registry are the entry points. The schema classes stay exported: callers need them to annotate what load_config returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
Third-party packages can now contribute model configs to the registries without anyone importing them. A distribution named `<registry prefix>-<model name>` that implements the `ssm_configs_config_path` hook is picked up on install: the prefix selects the registry and the remainder becomes the model name, so one distribution ships one model. Discovery is lazy -- it runs on the first read of a registry's `external_models`, never at import -- and warns and skips on an unknown prefix, a name already taken, a hook that raises, or a missing file, so one broken plugin cannot take the others down with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
The subpackage gets its own pytest configuration and test suite, so its tests live next to the code they exercise rather than in the root `tests/` tree. Coverage spans the manual registration path, real entry-point discovery, prefix routing, name normalization, laziness, and each warn-and-skip failure mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
The subpackage has its own pytest configuration, so the root `uv run pytest` does not collect it. Give it a step of its own across the version matrix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
The README gains the author-facing side: how to name a plugin distribution, the hook to implement, and the manual registration escape hatch. CLAUDE.md records that ssm_configs is a workspace subpackage owning its own tests, and how to run them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
The two suppressed errors had straightforward fixes: the spec is now a staticmethod, which is what the missing `self` argument was really about, and its body raises NotImplementedError rather than being docstring-only. Pluggy resolves the spec through `getattr` on the class and never calls its body, so neither change affects discovery. Verified clean under mypy, ty and pyrefly. ty caught a genuine slip in the tests along the way: `external_models` values are `str | Path`, so reading `.name` off one needs a `Path` first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
`hssm_example_model/` is a complete, standalone distribution that depends on nothing but `ssm-configs` and `pluggy`: a distribution name, an entry point, a hook returning a path, and a JSON config. It is the thing to copy when writing a plugin, and it doubles as proof that the mechanism works against a real installed package rather than only against hand-registered test doubles. It joins the workspace and the dev group so `uv sync --all-groups` installs it, which is what lets the test added next exercise the entry-point path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
Skipped when the example distribution is not installed, so the suite still runs against an environment without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@krishnbera I started with a barebones implementation of ssm_configs (package name pending). This PR demonstrates how the registry system works and how to extend the system with plugins. It also touches a bit upon how Pydantic should validate JSONs. Definitely open to suggestions @AlexanderFengler @cpaniaguam for awareness. Feel free to chime in too |
What
ssm_configsis forA small, separate package for describing models, split out from the code that runs them. A model config is just data — the model's name, its parameters and their allowed ranges, what the responses look like — so it doesn't need the simulators, NumPy, or a C compiler to be read. Keeping it standalone means HSSM and other tools can depend on the descriptions alone, and new models can be added without touching
ssm-simulators.Schemas and registries
BaseConfigSchemaholds what every model has (name, description, response columns, choices, parameter bounds);HSSMConfigSchemaandRLSSMConfigSchemabuild on it for their model families. Configs are stored as JSON and checked against the schema when loaded, so a malformed config fails immediately with a clear message instead of much later.hssm_registryandrlssm_registry— and each knows two kinds of model: built-in ones shipped with the package, and external ones contributed by other packages. You ask a registry whether it knows a model (is_supported) and to load it (load_config), and it doesn't matter which kind it is. Each registry is a single shared instance, so everything in a program sees the same set of models.The plugin system
Anyone can add a model by publishing a package — no changes to this repo:
hssm-my-cool-modeladdsmy_cool_modelto the HSSM registry.ssm_configsfinds it on its own.The example package
hssm_example_model/at the repo root is a complete working plugin and the thing to copy when writing one. It's a real, self-contained package — its ownpyproject.toml, depending only onssm-configs— with four files: the config JSON, a fifteen-line module with the hook function, thepyproject.tomldeclaring the entry point, and a README. To adapt it: rename the package, drop in your own JSON, done. It's installed in this repo's dev environment, so the tests check the plugin path against a genuinely installed package rather than a stand-in.Workspace change
ssm-simulatorsat the root withssm_configsandhssm_example_modelas members. One shared environment and oneuv syncfor all three, while each stays a separately publishable package.ssm_configsis installed through the dev group, not as a runtime dependency, while it's still settling. The oldconfigextra is gone.uv.lockis no longer tracked, since the workspace regenerates it.Tests and CI
ssm_configs/tests/covering both ways a plugin gets registered, routing to the right registry, the lazy scan, and every failure case.ruff,mypy,ty, andpyrefly.🤖 Generated with Claude Code
https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA