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 — Mediummedium):

  • Letters, digits, _, and - only
  • Single path segment (no /, \, ., or ..)
  • Create files by hand in v1 (no ninja profile subcommands)

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_loop
  • plan_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

  1. Load base config.json (tools, cache, base models, plan_panel, …).
  2. If a profile is selected, load profiles/<name>.json and overlay model fields.
  3. 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.