Model profiles
Switch named model layouts at launch without editing base config.json.
A profile is a small JSON file that overlays model fields only onto your usual config. Tools, cache, retry, and the plan panel stay global.
Select
# Interactive session with the "local" layout
ninja --profile local
# Same via env (flag wins if both are set)
NINJA_PROFILE=Medium ninja
# Profile + headless: -p is still the print prompt, not a profile short flag
ninja --profile local -p "fix tests" --yes
| Source | When it applies |
|---|---|
ninja --profile <name> |
Always wins when the flag is non-empty |
NINJA_PROFILE=<name> |
Used when the flag is unset / empty |
Bare ninja |
No profile — base config.json only |
There is no “last active” profile and no mid-session switch in v1. Opt-in only.
**Never use -p for profiles.** -p / --print is headless print mode and stays that way.
Missing or invalid profile names fail launch (exit 1) with the absolute expected path — Ninja does not silently fall back to base-only.
Storage
Profiles live next to your main config, under profiles/:
| OS | Path |
|---|---|
| Linux | ~/.config/ninja/profiles/<name>.json |
| macOS | ~/Library/Application Support/ninja/profiles/<name>.json |
| Windows | %APPDATA%\ninja\profiles\<name>.json |
Name rules (case-sensitive on disk — Medium ≠ medium):
- Letters, digits,
_, and-only - Single path segment (no
/,\,., or..) - Create files by hand in v1 (no
ninja profilesubcommands)
File mode is 0o600; the profiles/ directory is 0o700 when Ninja creates it (e.g. on /model save).
What a profile owns
Included (same field names as base config — no new vocabulary):
| Key group | Fields |
|---|---|
| Planner scalars | openai_base_url, openai_api_key, openai_model |
| Planner pin | planner (base_url, api_key, model) |
| Subagent defaults | ninja_subagent_base_url, ninja_subagent_api_key, ninja_subagent_model |
| Legacy subagent keys | subagent_base_url, subagent_api_key, subagent_model |
| Roles | subagent_roles (per-role base_url, api_key, model, …) |
Not profile-scoped (always base config.json):
enabled_tools,cache,retry,doom_loopplan_panel(multi-model planning seats)post_edit_check,force_fresh_coders,update_check, skills paths, etc.
Unknown non-model keys in a profile file produce a warning and are ignored (so a full config.json paste still loads model fields only).
Overlay
- Load base
config.json(tools, cache, base models,plan_panel, …). - If a profile is selected, load
profiles/<name>.jsonand overlay model fields. - Run the normal resolve ladder (planner pin, env, roles) on the merged config.
| Field | Rule |
|---|---|
| Scalar model strings | Non-empty profile value replaces base |
planner |
If the profile has a planner object, it replaces the whole base planner pin |
subagent_roles |
Per role name: profile entry replaces that role’s whole config; roles only in base remain |
Empty {} is a valid no-op profile. Omitted fields keep base values. After overlay, env vars still work as usual (OPENAI_MODEL can shadow openai_model unless a planner pin wins).
Examples
Fully local — profiles/local.json
{
"planner": {
"base_url": "http://localhost:8080",
"model": "local-planner-A"
},
"ninja_subagent_base_url": "http://localhost:8080",
"ninja_subagent_model": "local-coder-C",
"subagent_roles": {
"coder": { "base_url": "http://localhost:8080", "model": "local-coder-C" },
"reviewer": { "base_url": "http://localhost:8080", "model": "local-reviewer-B" },
"final_review": { "base_url": "http://localhost:8080", "model": "local-reviewer-B" }
}
}
ninja --profile local
Hybrid cloud + local — profiles/Medium.json
Partial overlays are fine: only override what differs from base.
{
"planner": {
"base_url": "https://api.example.com/v1",
"api_key": "REPLACE_PLANNER_KEY",
"model": "strong-planner"
},
"ninja_subagent_base_url": "http://localhost:8080",
"ninja_subagent_model": "local-coder",
"subagent_roles": {
"coder": {
"base_url": "http://localhost:8080",
"model": "local-coder"
},
"reviewer": {
"base_url": "https://api.example.com/v1",
"api_key": "REPLACE_REVIEWER_KEY",
"model": "review-model"
}
}
}
NINJA_PROFILE=Medium ninja
# or
ninja --profile Medium
Unspecified roles (e.g. final_review if omitted) keep whatever base config.json defines.
/model save
In the TUI, /model (or /model set …) can persist a pick.
| Active profile? | Target | Where save writes |
|---|---|---|
| No | any | Base config.json (unchanged behavior) |
| Yes | planner or a role | Profile file only — patches that target in profiles/<name>.json |
| Yes | plan-panel seat | **Base config.json only** — plan_panel is not profile-scoped |
Session overrides still apply in memory first; save remains optional. Toasts distinguish “saved to profile …” vs “saved to config.json (plan_panel is not profile-scoped)”.
Doctor
When a profile is active, ninja doctor (or ninja --profile local doctor) reports it:
PASS profile "local" (flag) overlay /home/you/.config/ninja/profiles/local.json
Source is flag or env. Load warnings (e.g. ignored non-model keys) show as WARN. A missing profile is FAIL and stops diagnostics for that overlay.
With no profile selected, doctor skips the profile line.