Contributing¶
Thanks for considering a contribution.
Dev setup¶
Clone and install with all extras:
Or with uv (faster):
For gated HuggingFace models, log in once:
Run the tests¶
The default suite is mock-based and needs no backend:
To run against a real backend:
pytest tests/test_models.py --client=ollama --model=QWEN_3_5_9B
pytest tests/test_models.py --client=anthropic
pytest tests/test_models.py --client=llamacpp --model-path=/path/to/model.gguf
See CLAUDE.md for the full list of --client options.
Linting¶
Ruff is configured in pyproject.toml (line length 120):
Build the docs locally¶
The site rebuilds on file changes. Browse at http://127.0.0.1:8000.
To check for broken links and missing pages:
Work on the notebooks¶
The notebooks/ collection is authored as plain-text Quarto .qmd
files (markdown with executable python cells), not .ipynb JSON, so they diff cleanly
and are easy to edit or hand to an AI assistant. Install the Quarto CLI
(a standalone binary, e.g. brew install quarto) plus the Python toolchain:
pip install -e '.[notebooks]' # or: uv sync --extra notebooks — installs jupyter + jupytext
quarto preview notebooks/ # browsable local render of the whole collection
quarto preview/render produce the rendered site; they are not how you iterate
cell-by-cell. For interactive execution, pick one:
- Convert to
.ipynb(best inline experience).python notebooks/convert.py to-ipynbgenerates local, git-ignored.ipynbyou open natively in VS Code or Jupyter with inline cell execution..qmdstays the source of truth, so after editing a notebook runpython notebooks/convert.py to-qmd <stem>to sync it back (the helper defends against the string-source newline mangling noted below). - VS Code + Quarto extension. Runs
.qmdcells in the Jupyter Interactive Window (a side panel, not inline). A.vscode/settings.jsonpins the.venvinterpreter; rebind "Quarto: Run Cell" (Ctrl+Shift+Enter) toShift+Enter(when: editorLangId == 'quarto') for Jupyter muscle memory. - JupyterLab via jupytext (in the
[notebooks]extra). Runjupyter laband open a.qmddirectly as a native notebook; edits save back to the.qmdplain text.
Conventions:
- Naming. Kebab-case with a zero-padded ordinal, e.g.
07-agents.qmd. The number sets the reading order (and the sidebar order innotebooks/_quarto.yml). - Not executed at render time.
_quarto.ymlsetsexecute: eval: falsebecause most notebooks need a live backend (Ollama, a cloud API key, or a GPU) and gracefully skip. Run cells yourself against your own backend. To bake real outputs for a cheap (e.g. Ollama-only) notebook, seteval: true+freeze: autoin that notebook's own front matter. - Convert an existing
.ipynb.quarto convertmangles string-typed cell sources, so normalize them to line lists first:python -c "import json,sys; nb=json.load(open(sys.argv[1])); [c.__setitem__('source', c['source'].splitlines(keepends=True)) for c in nb['cells'] if isinstance(c['source'],str)]; json.dump(nb, open(sys.argv[1],'w'))" nb.ipynbthenquarto convert nb.ipynb.
Coding conventions¶
- Plain Python over framework primitives. No
Runnableprotocol, noBaseTool, no LCEL|, no Pydantic for tool args. See design principles for the full list of what's deliberately out of scope. - OpenAI message dicts as the only data model. No
Messageclass. Provider-specific formats are adapted at request time, never persisted. - Loud failures. Bad input raises with an actionable message. Silent skips and
try/except: passare reviewed carefully. - Legibility beats convenience. AIMU exists so people can find out what models and systems can actually do, so the question a review asks is: does this make a model's real behaviour more legible, or does it hide it? A change that is more convenient but obscures what the model did (a silent fallback, a swallowed capability mismatch, a wrapper that hides the request) is working against the point, however clean it looks.
Adding a new provider¶
Write a client module under aimu/models/providers/ (a flat providers/<name>.py, or a providers/<name>/<modality>.py subpackage only if the provider ships several standalone modality clients), subclass BaseModelClient (or OpenAICompatClient for OpenAI-compatible endpoints), wire it into the ModelClient factory and _provider_registry(), export it from aimu/models/__init__.py under a HAS_* flag, and mirror it on the async surface.
See how-to: add or update a provider for the full step-by-step, and an existing provider (e.g. providers/anthropic.py) for the pattern.
Adding a new model to an existing provider¶
For the three cloud catalogs (AnthropicModel, OpenAIModel, GeminiModel), add a member with a ModelSpec(id, tools=..., thinking=..., vision=...) value directly. For every other (local-runtime) catalog, add the model's intrinsic facts to the shared MODEL_FACTS table in aimu/models/_catalog.py once (skip this if another local-runtime catalog already carries the name), then add MODEL_NAME = Wire(id) to the provider's Model enum -- capability flags resolve from MODEL_FACTS automatically, and an override of an intrinsic flag needs a why=. The TOOL_MODELS / THINKING_MODELS / VISION_MODELS lists derive automatically either way. See how-to: add a new model.
Pull requests¶
- One concern per PR. Splitting a refactor across multiple PRs makes review tractable.
- Include or update tests. Each module has a dedicated test file (
tests/test_models_api.py,tests/test_tool_decorator.py,tests/test_workflow_chain.py, etc.). Add new behavioural tests next to the existing ones for the surface you're touching. - Update docs when adding a public API. New
@tool-decorated function? Add it tobuiltin.<group>. New workflow? Add a tutorial or how-to. - Run
ruff check .andpytestbefore pushing.
Reporting bugs¶
File an issue with:
- A minimal reproducer (under 20 lines if possible).
- The full traceback.
- AIMU version, Python version, and which provider you were using.
Questions¶
Open a GitHub discussion. For Claude Code agents working in this repo, CLAUDE.md is the canonical engineering reference.