Contributing
HybridInference is MIT-licensed (see LICENSE) and contributions are welcome.
This page covers what the repository contains, the checks a change has to pass,
and how a change gets proposed.
Getting set up
git clone <your-fork-url> hybridinference
cd hybridinference
git remote add upstream https://github.com/HarvardMadSys/hybridInference.git
git fetch upstream dev
git switch -c yourname/tests/routing-config-defaults upstream/dev
make setup-dev
Replace yourname in the branch name with your GitHub username. To keep your
main checkout free for other work, create the branch in a separate
worktree instead, for example
git worktree add -b <branch> ../hybridinference-<feature> upstream/dev. If you
cloned the upstream repository rather than a fork, start from origin/dev and
push to your fork remote.
make setup-dev creates .venv on Python 3.12, installs the project editable,
syncs the dev dependency group and installs the pre-commit hooks. Full
prerequisites and the ways to run the gateway are in
Installation.
Every make target that touches Python runs through uv run. Override that if
your environment needs it:
make lint UV_RUN="uv run --active"
Your first contribution
Start by running the Quickstart in your worktree, or use the source development setup. Seeing the gateway answer one request makes it easier to tell whether a later failure comes from your change or from setup. A local example deployment is enough; contributing does not require access to a maintainer’s server.
Choose a small reproducible bug, an uncovered configuration case, or a confusing instruction you can verify yourself. Search the issue tracker and existing tests first. For a larger feature, open a feature request to agree on the behavior before writing it; the repository provides bug and feature forms.
For a concrete first exercise, cover a routing YAML endpoint that uses an
environment-variable default. Read _expand_env_value and
load_routing_config in apps/backend/routing/config.py, then the neighboring
tests in tests/unit/routing/test_config.py. An unset variable should use the
default URL; a set variable should override it. The test below exercises both
through the public loader and restores the environment after each case:
@pytest.mark.parametrize("endpoint", [None, "https://override.example"])
def test_endpoint_env_default(tmp_path, monkeypatch, endpoint):
monkeypatch.delenv("TEST_ROUTING_ENDPOINT", raising=False)
if endpoint is not None:
monkeypatch.setenv("TEST_ROUTING_ENDPOINT", endpoint)
path = tmp_path / "routing.yaml"
path.write_text(
"remote_deployment:\n"
" - endpoint: ${TEST_ROUTING_ENDPOINT:-https://default.example}\n"
" models: [example-chat]\n"
)
config = load_routing_config(path)
assert config.remote_deployment[0].endpoint == (endpoint or "https://default.example")
assert config.remote_deployment[0].models == ["example-chat"]
Add it to that test file only if the case is still uncovered. This is a test-only contribution when the loader already behaves correctly. For a bug fix, first make your regression test fail on the original code, then change the smallest responsible function until it passes.
Run the focused tests while editing:
uv run pytest -q tests/unit/routing/test_config.py
For a runtime change, repeat the affected request against your local gateway.
With the Quickstart’s example running,
make build s=backend DISTRIBUTION=example rebuilds
changed backend code; make smoke DISTRIBUTION=example checks health, the
model list and a routed completion. Exercise the changed behavior as well:
a generic smoke passing does not establish that a particular bug is fixed.
The Quickstart explains the separate full-console check
when your change involves accounts or the frontend.
Before opening the PR, run make format, make lint and make test, plus the
frontend or documentation checks when relevant.
Review git diff so the patch contains only your intended files. Push your
branch to your fork and open a PR targeting upstream dev, using a title such
as test(routing): cover endpoint environment defaults. Fill in the existing
PR template with the behavior covered and the exact checks you ran; include
the reproduction and before/after result for a bug fix.
Repository layout
apps/
backend/
serving/ FastAPI gateway: HTTP surface, SSE streaming, provider
adapters, auth, storage, observability, admin API
routing/ routing engine: routers, strategies, endpoint health,
circuit breaker
frontend/ Next.js web and admin console
config/
examples/ reference model registry and routing config; also the
built-in fallback a checkout with no overlay resolves to
distributions/ deployment overlays, one directory each: manifest, config,
branding, deploy env files. `example/` is the runnable
example the Quickstart uses
deploy/
docker/ Dockerfiles and docker-compose.yml
systemd/ unit files for host-level deployments
docs/
developer/ this guide (MyST Markdown, built with Sphinx)
agents/ design specs and plans
ops/ maintenance and CI helper scripts (admin, ci, db)
tests/ see the test tiers below
benchmark/ benchmarking scripts
Two things about the backend are easy to get wrong:
apps/backend/routing/executor.pyis a backward-compatibility shim that re-exportsFixedRouterasRouteExecutor. Editapps/backend/routing/routers.pyinstead.The backend packages are
servingandrouting, rooted atapps/backend.make setup-devinstalls them into.venv, souv runor an activated.venvimports them from any directory; outside that environment, setPYTHONPATH=apps/backend.
Quality gates
Command |
What it runs |
|---|---|
|
|
|
|
|
|
|
|
|
|
make all does not type-check. mypy is in the dev dependency group and
you may run it by hand, but pyproject.toml has no [tool.mypy] section and no
target invokes it, so nothing enforces it.
Style is enforced by tooling, not by review:
ruff, line length 100, target
py310, withE/W/F/I/UP/B/C4/SIM/TCH/RUFenabled ([tool.ruff]inpyproject.toml).pydocstyle, Google convention. It skips
tests,.venv,node_modules,apps/frontend,opsanddocs, so docstrings are required inapps/backendand enforced there.
Pre-commit hooks run on commit and are installed by make setup-dev:
pre-commit run --all-files # run them over the whole tree
They cover ruff (pinned to the version CI uses), pydocstyle, gitleaks secret
scanning, the standard whitespace/YAML/JSON/TOML checks, and eslint + prettier
for apps/frontend.
Before opening a pull request, run make format and make sure make test
passes.
Tests
Markers are the contract; directories are a convention. The markers declared in
pyproject.toml are unit, integration, slow, perf, external and
dbtest, and --strict-markers means an undeclared marker is an error.
Tier |
Lives in |
Marker |
In |
|---|---|---|---|
Unit |
|
— |
yes |
API surface |
|
— |
yes |
Server / observability |
|
— |
yes |
Needs a live Postgres |
mostly |
|
no |
Hits live external servers |
|
|
no |
testpaths is ["tests", "distributions"], so overlay-owned tests run in the
default suite alongside tests/.
make test # the default suite
make test-verbose # same selection, serial, -vv
make test-cov # same selection, with coverage
make test-db # only -m dbtest
make test-all # everything except -m external
make test-external # only -m external
uv run pytest tests/unit/routing/test_manager.py # one file
uv run pytest -m dbtest tests/integration/ # one tier
The dbtest tier needs a reachable PostgreSQL. Tests read TEST_DB_HOST,
TEST_DB_PORT, TEST_DB_USER, TEST_DB_PASSWORD (defaulting to localhost:5432
and postgres/postgres), and some read a full TEST_PG_DSN. Starting just
the database container is enough:
docker compose -f deploy/docker/docker-compose.yml --env-file .env up -d postgres
make test parallelises with pytest-xdist at file granularity. Run serially
when you are debugging a specific failure, but check a suspected env-leak
failure under -n auto too — cross-file environment leakage only shows up
under the parallel run.
Frontend
The console’s gates are separate from the Python ones:
make frontend-install # npm ci
make frontend-lint # eslint, --max-warnings 0
make frontend-type-check # tsc --noEmit
make frontend-test # vitest run
make frontend-check # all three
make check-all # backend lint + test, then frontend-check
CI additionally runs npm run format:check (prettier) and
npm audit --omit=dev --audit-level=high in apps/frontend.
Documentation
These pages are MyST Markdown compiled by Sphinx from docs/developer/, with
docs/developer/index.rst as the root toctree. Build them, and run the checks
CI runs, from the repository root:
make docs-verify
That builds the English and Chinese sites with warnings treated as errors,
checks the local links in every Markdown file, and checks that no translated
paragraph has quietly fallen back to English. A broken cross-reference or a page
missing from the toctree fails it, so run it before pushing. Sphinx,
myst-parser and sphinx-rtd-theme come from the dev dependency group, so
make setup-dev is enough to build. make docs builds the site alone.
Add a new page to docs/developer/index.rst as well as writing it; an orphan
file is a warning, and warnings are errors in CI.
Writing a page
These pages ship with the source, and most readers arrive from a search or the sidebar with one task in mind. Write for that reader:
Say who the page is for and what they can do after reading it, in the first paragraph.
Lead with the action or the answer. Put exceptions and edge cases after it, in a note if they run long, and do not open a section with what the software lacks.
Name a file only when the reader will open or edit it. Do not cite functions or line numbers to prove a statement; tests and review do that.
Describe the software as it is now. What changed, how it used to work and how you checked belong in the pull request, the release notes or
docs/reviews/.Leave out the details of any one installation — its hosts, model ids, accounts and measurements. They belong in that distribution’s own documentation.
Use the words in the Glossary, and add a term there before relying on a new one.
Keep a paragraph to one idea. When it piles up conditions, make it a list or a table.
When a setting does not do what its name suggests, say so in one sentence, add it to Settings that currently have no effect, and open an issue, rather than explaining the internals at length.
Translations
The site is published in English and Chinese, and an English edit changes the paragraph a translation is attached to. Update the Chinese in the same pull request; Translating the Docs explains how.
Proposing a change
Branch off
dev, notmain. Pull requests targetdev; CI runs on pull requests againstmainanddev.Name branches
<user>/<scope>/<feature-name>, e.g.jane/routing/weighted-fallback.Write commit and pull-request titles as conventional commits —
type(scope): summary, e.g.fix(routing): keep the fallback route on 429. History ondevis squashed one commit per pull request, so the title you write becomes the commit subject —git log --onelineshows the shape to match.Keep a pull request to one feature or fix, update the docs in the same change, and add tests for new behaviour.
Call out changes to API behavior, configuration, defaults or database schema in the PR, with migration steps and rollback limits. See Releases and upgrades.
Do not commit to
mainordevdirectly.
What CI will run
The CI workflow (.github/workflows/ci.yml) classifies which paths a pull
request touched and then runs only the relevant jobs:
Job |
What it does |
|---|---|
Backend Quality |
|
Frontend Quality |
prettier, eslint, |
Site UI Containers |
builds the frontend image with no module and with the example’s Site UI module, requires a module build that lacks its context to fail, and checks how the running example serves its assets |
Docs Build |
|
|
pytest across four shards with a PostgreSQL service, |
Security Scan |
|
|
builds the affected images, then starts the runnable example and smokes it |
Tutorial E2E |
runs the Quickstart’s |
CI Gate |
aggregates the results of the jobs above into one check |
Because CI includes dbtest, a change to storage or auth can be green locally
and red in CI. Run make test-db against a local Postgres when you touch
apps/backend/serving/storage/ or the auth surface.
If a pull request shows no CI run at all, check whether it has a merge conflict
first — a conflicted pull request does not trigger the pull_request workflow.