Contributing¶
Thanks for helping grow the catalog. The whole point of biolm-hub is uniformity — the diff
between any two models should be the science, not the plumbing. These are the rules that keep it that
way. (The why behind them is in PHILOSOPHY.md.)
Getting set up¶
make install # venv + all dev deps via uv, and pre-commit hooks
make style # ruff + black + hygiene hooks
make mypy # static type checking (enforced, --strict)
make test-unit # fast tests — no Modal, no R2
make check runs the every-PR safe gate: style + mypy + schema-doc check + CI-script tests + unit
tests (no Modal, no R2). Keep it green locally before you push. The docs build (make docs, i.e.
mkdocs build --strict) is a separate check — run it after touching any schema, Field(description=),
or root doc.
Auto-activate with direnv (optional)¶
The repo ships a committed .envrc so direnv gives you "cd in and everything
just works": the uv .venv is on your PATH (so bh, pytest, pre-commit resolve directly) and a
local .env is loaded — no source .venv/bin/activate needed.
brew install direnv # then add the shell hook: https://direnv.net/docs/hook.html
direnv allow # once per clone/worktree, and after editing .envrc
cp .env.example .env # optional: your local Modal/R2 config (gitignored)
.env.example documents every environment variable the repo reads — all optional. Real secrets live
only in the gitignored .env; never commit .env. Without direnv, source .venv/bin/activate (or
uv run …) works exactly as before.
Adding a model¶
Using an AI coding agent? This guide is the policy & house-rules reference; the step-by-step build recipe lives in the Claude Code skills under
.claude/skills/. Point the agent atmodel-implementation(implement → validate → deploy → document → review) andmodel-knowledge-base(the five knowledge-graph files) — both defer to this document for policy. Humans: read on. Either way, start frommodels/dummy/.
Start from models/dummy/ (the template) and keep the standard layout:
models/<name>/
app.py # the Modal app + the action methods
config.py # ModelFamily: variants, action schemas, the modal_class_name
schema.py # request/response Pydantic models
test.py # the TestSuite (integration + deployment cases)
download.py # (if the model has weights) how to fetch them
sources.yaml # license, papers, source repos ┐
comparison.yaml # strengths/weaknesses, when-to-use, alts │ the knowledge graph —
README.md # API reference │ required, not optional
MODEL.md # architecture, training, benchmarks │
BIOLOGY.md # the biology, applied use-cases ┘
License first — this section is canonical (the skills defer to it). Accepted: MIT, Apache-2.0,
BSD-3-Clause (and compatible permissive licenses), plus CC-BY-4.0 (common for model weights).
GPL / copyleft is accepted only after a maintainer reviews the copyleft reach for
redistribution and serving — flag it in the PR and ask; don't assume. Not accepted: CC-BY-NC and
other non-commercial or "academic only" terms, and proprietary licenses. Code and weights can carry
different licenses (e.g. MIT code, CC-BY-NC weights) — the more restrictive one governs. Declare the
license in sources.yaml, include a per-model LICENSE/attribution file, and never vendor weights or
code you can't redistribute.
Scope — bounded assets only. A model's assets must be either (a) bounded weights that download.py
fetches through the r2_then_* wrappers (self-populating the public R2 bucket), or (b) provided by the
caller in the request. Models that require a Modal Volume, a server-side reference database
(UniRef / BFD / MGnify / PDB70 / …), or server-side MSA/template search are out of scope for this
catalog — that volume lifecycle, multi-TB data hosting, and per-caller cost attribution is deliberately
omitted here. Alignment-dependent models take the MSA as an msa/alignment input (see chai1, rf3,
msa_transformer): the caller supplies the alignment; the endpoint does not search a hosted database.
CI enforces this (tooling/check_no_modal_volumes.py).
House rules (the "Global Rules")¶
These are uniform across every model. CI and review enforce them.
Actions¶
The canonical action set is closed:
| Verb | Means |
|---|---|
predict |
a scalar/label property of a sequence or structure — or masked-token / fill-mask prediction (mind the payload for large-vocab LMs; the model-implementation skill has the detail) |
fold |
3D structure prediction (returns pdb/cif + confidence) |
encode |
learned representations / embeddings |
generate |
produce new sequences or structures (sampling, infilling, inverse folding, design) |
score |
a model-defined scalar fitness (document what it means) |
log_prob |
a per-sequence (pseudo) log-likelihood scalar |
Pick the verb that matches the intent. A folding model uses fold, not predict. Don't invent new
verbs.
Schema field names¶
Field names are uniform across families; the biology lives in the model's metadata/tags, not in the field names:
- Inputs:
sequence/sequences/msa;pdb/cif;smiles(+ccd);name; batch items underitems, parameters underparams. - Antibodies:
heavy_chain/light_chain. A nanobody/VHH is a loneheavy_chainon a model tagged as single-domain — there is novhh/nanobodyfield. TCR chains aretcr_alpha/tcr_beta/tcr_gamma/tcr_delta, pluspeptideandmhc. - Outputs:
embeddings,logits,log_prob,score, generatedsequence,pdb/cif,plddt/ptm/pae; batch results underresults.
When renaming for compatibility, keep the old name working via a Pydantic field alias.
Every field must carry Field(..., description="...") — that description is the only thing that
renders in the OpenAPI/JSON schema and on the docs site (plain # comments do not). For shared field
names, reuse the canonical wording in tooling/field_glossary.yaml so descriptions don't drift across
models; tooling/check_schema_docs.py (a unit test) fails CI on an undocumented field or one that
diverges from the glossary.
Logging¶
Use the shared logger; never print in runtime code:
from models.commons.core.logging import get_logger
logger = get_logger(__name__)
print() is rejected by lint everywhere except the CLI, scripts, and tests. Levels: debug for
internals, info for lifecycle, warning for degraded/fallback, error for failures (with
exc_info=True). Never log full sequences or secrets.
Errors¶
Raise a typed user error for a caller's mistake (it's surfaced verbatim with a stable code); let
system errors propagate (they're sanitized). Never raise a bare Exception/ValueError for bad
input, and never catch-and-print.
Code style¶
Modern, typed Python: full type hints (mypy is enforced), Pydantic v2, pinned exact dependency
versions, ruff + black. Run make style && make mypy before pushing.
Testing¶
Tests are the coherence mechanism. There are four tiers:
| Tier | What | Needs |
|---|---|---|
| Static | make style, make mypy, make check-schema-docs, pytest --collect-only |
nothing |
| Unit | make test-unit |
nothing |
| Integration | deploy to a Modal env + golden fixtures | Modal env + R2 |
| Deployment | run against a live endpoint | Modal env + R2 |
- Generate fixtures, then run the file: run
python models/<name>/fixture.pyto produce the golden fixtures, thenpython -m pytest models/<name>/test.py. Writing public goldens needs your own R2 bucket + credentials; the public catalog's goldens are maintainer-populated (themodel-implementationskill'svalidation/GUIDE.mdhas the mechanics). - The golden output is the oracle — don't regenerate goldens to force a test green. Regenerate only when an output change is intended, and say so in the PR.
- Reuse shared test assets rather than hardcoding a standard sequence in your fixture. Standard
cross-model inputs live in
models/commons/testing/shared_assets.pyas importable constants (e.g.from models.commons.testing.shared_assets import STANDARD_PROTEIN). Larger shared inputs live in public R2 under the canonical conventiontest-data/shared/<category>/<name>.<ext>— a fixture path beginning withshared/resolves there instead of your per-model directory. - Cover the testable code paths you add. Coverage is a local diagnostic, not an enforced gate —
make test-unitand CI run with--no-covfor speed; runuv run pytest(no--no-cov) to see a report with missing lines.
Verify your model¶
Before opening a PR for a new or changed model, confirm it lands in house style and actually runs:
make checkmust be green — this is the every-PR safe gate (style, mypy, the schema-doc check, CI-script tests, and unit tests). It runs Modal-free, so anyone can run it locally.- Added or renamed a model? Regenerate the catalog index —
python -m tooling.gen_model_catalog, then commitmodels/README.md. Skipping this leavesmake checkred on the stale index. make docsmust build (mkdocs build --strict) — your model's page is generated from its config + knowledge graph, so schema or KB mistakes surface here.- Generate goldens and run the model's test file against a Modal deployment:
python -m pytest models/<name>/test.py. The golden output is the oracle — regenerate goldens only when an output change is intended, and say so in the PR. Integration and deployment tests (which need a Modal env + R2) run in the maintainer-gateddeploy.ymlworkflow once a maintainer applies thedeploy-approvedlabel and approves thebiolm-hub-devdeploy (see Continuous integration and deploys).
Pull requests¶
- One coherent change per PR; keep
make checkgreen. - Fill out the PR template (
.github/PULL_REQUEST_TEMPLATE.md) — it's the house-rules checklist. - For a new or significantly changed model, have a fresh-context reviewer (a different person, or a fresh agent session) read the full diff — same-context self-review misses things.
- Fix failures locally before pushing — don't push just to re-trigger CI.
- Be kind and assume good faith.
Continuous integration and deploys¶
CI is two-tier, split by trust — cheap, safe checks run automatically, while anything that spends Modal/R2 or touches secrets is maintainer-gated.
Tier 1 — automatic, no secrets (.github/workflows/ci.yml). Runs on every push and PR (forks
included), because it never needs Modal, R2, or any secret:
- lint · types · unit — style (ruff + black + hooks) + mypy + the schema-doc check + the CI
change-detection script tests + unit tests. This job is exactly
make check, so keep it green locally before you push. - docs build —
mkdocs build --strict(the same asmake docs). - secret scan — gitleaks.
Tier 2 — maintainer-gated, secret-bearing (.github/workflows/deploy.yml). The expensive Modal
deploy + integration/deployment tests. Opening or updating a PR does not trigger this — it runs
only when a maintainer clears both gates:
- adds the
deploy-approvedlabel to the PR (applying a label needs repo write, so only a maintainer can start the pipeline), and - approves the
biolm-hub-devGitHub Environment — the deploy job pauses for a required reviewer, and the approval prompt shows the exact commit SHA before any secret is exposed.
Once both gates pass, the affected models — computed by .github/scripts/detect_models.py, where a
change under models/commons/ fans out to every model that imports it — are deployed to the
biolm-hub-dev Modal environment and their integration + deployment tests run. A maintainer can also
run the pipeline manually from the Actions tab (workflow_dispatch), passing the model slugs to
deploy.
The deploy-approved label pins the run to the commit it was added on; a later push does not
deploy, so the code that runs is exactly what was reviewed. To ship a newer commit, re-review and then
remove and re-add the label.
Maintainers: approving runs the PR's code (
config.py/app.py) on Modal with credentials in scope, so review the full diff before labeling (one approval can fan out to every model ifmodels/commons/changed). One-time setup: create thedeploy-approvedlabel and abiolm-hub-devGitHub Environment with required reviewers, and storeMODAL_TOKEN_*,R2_*, and theBIOLM_HUB_DEV_ENVIRONMENT_SENTINELas environment secrets (never repo-wide — the workflow's preflight fails fast otherwise). Scope the Modal token to thebiolm-hub-devworkspace and the R2 creds to a read-only dev bucket. Full details are in the header of.github/workflows/deploy.yml.