# ADR-0014: Serve Small Models on Modal, One App Per Provider ## Status Accepted. **Amended by [ADR-0019](0019-single-model-catalogue-no-cloud-path.md):** the model catalogue moved to the stdlib-only `modal/catalogue.py` (single source of truth, shared with the engine); `registry.py` is now a back-compat re-export, and the engine binds endpoints from the catalogue + `MODAL_WORKSPACE` rather than `OPENAI_BASE_URL`. ## Context The engine routes agent roles to small models through an OpenAI-compatible interface, but until now there was no hosted backend behind it — only the local deterministic stub. We need real small models (all under the 32B cap, with a ≤4B Tiny Titan tier) served as APIs the engine can call, without coupling the engine to any single inference vendor. We want the serving layer to be scalable (autoscaling, pay-per-use), extensible (adding a model or provider should be trivial), and configurable per task (GPU, context length, concurrency, tool/reasoning parsers, multimodal limits). ## Decision Add a `modal/` folder that serves models on Modal as serverless, OpenAI-compatible endpoints (vLLM behind an autoscaling web server). - **One Modal app per provider** (`nvidia-llms`, `openbmb-llms`, `google-llms`). Providers deploy, scale, and fail independently. - **One reusable serving path** in `service.py` (`ModelConfig` + `register_model`) shared by every app, so the vLLM/Modal best practices are written once. - **Configuration is data** in `registry.py`: a model is one `ModelConfig`; a provider is one app file. This mirrors the project's "config, not code" invariant (ADR-0011). - Weights and the vLLM compile cache live in **shared Volumes**, so a model pulled once is warm across every provider app. ## Consequences - The engine talks to any endpoint via the OpenAI SDK by setting `OPENAI_BASE_URL`; model roles (`MODEL_TINY/FAST/BALANCED/STRONG`) map to the endpoint whose size fits the role. - Vendor isolation: a provider can be added, retuned, or removed without touching the others or the engine. - Gated repos (Gemma, the Nemotron repos used here) require a Hugging Face token in the `huggingface-secret` Modal Secret; ungated models deploy without it. - Endpoints are public by default; bearer-token auth is opt-in at deploy time (`MODAL_LLM_REQUIRE_AUTH=1`) and supplied via the `llm-api-key` Secret as the `VLLM_API_KEY` env var — secrets are never hard-coded. - The API is OpenAI-compatible: each endpoint self-documents at `/docs` and `/openapi.json`, and a checked-in `modal/openapi.yaml` (3.1) plus `modal/docs/openapi.md` document the shared surface. - vLLM tool/reasoning parser names are version-specific and left conservative; enable per model once verified against the deployed vLLM version. - Modal's docs index is mirrored at `modal/docs/modal-llms.txt` and refreshed when the pinned vLLM/Modal versions change (ADR-0004, document-as-we-build).