Skip to content

Add ssm_configs: model config schemas, registries, and a plugin system - #354

Open
digicosmos86 wants to merge 23 commits into
feat-config-registryfrom
353-config-refactor-registries-for-configs
Open

Add ssm_configs: model config schemas, registries, and a plugin system#354
digicosmos86 wants to merge 23 commits into
feat-config-registryfrom
353-config-refactor-registries-for-configs

Conversation

@digicosmos86

Copy link
Copy Markdown
Collaborator

What ssm_configs is for

A 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

  • Schemas say what a valid config looks like. BaseConfigSchema holds what every model has (name, description, response columns, choices, parameter bounds); HSSMConfigSchema and RLSSMConfigSchema build 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.
  • Registries are the lookup tables. There's one per model family — hssm_registry and rlssm_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:

  • Name the package after the registry it belongs to plus the model name: hssm-my-cool-model adds my_cool_model to the HSSM registry.
  • Declare one entry point and one small function returning the path to your JSON config. That's the whole contract.
  • Installing the package is enough. Nobody has to import it — ssm_configs finds it on its own.
  • The scan happens the first time a registry is actually used, not when the package is imported, so it costs nothing if you never look up a model.
  • A broken plugin is skipped with a warning naming the package, and everything else keeps working. A plugin can't take over the name of a built-in model.

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 own pyproject.toml, depending only on ssm-configs — with four files: the config JSON, a fifteen-line module with the hook function, the pyproject.toml declaring 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

  • The repo is now a uv workspace: ssm-simulators at the root with ssm_configs and hssm_example_model as members. One shared environment and one uv sync for all three, while each stays a separately publishable package.
  • ssm_configs is installed through the dev group, not as a runtime dependency, while it's still settling. The old config extra is gone.
  • uv.lock is no longer tracked, since the workspace regenerates it.

Tests and CI

  • 15 tests in ssm_configs/tests/ covering both ways a plugin gets registered, routing to the right registry, the lazy scan, and every failure case.
  • The subpackage keeps its tests next to its code, so the root test run doesn't collect them — CI runs them in a step of their own across Python 3.12/3.13/3.14.
  • Clean under ruff, mypy, ty, and pyrefly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MNAryd7vmPQvf8KD59p5PA

digicosmos86 and others added 23 commits September 4, 2026 10:25
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
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: bfda67fb-3d16-4a0e-a7ab-497b76324118

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@digicosmos86

Copy link
Copy Markdown
Collaborator Author

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant