Skip to content

Migrating Model Configuration

Netclaw used to store each model role as an inline object under Models. It now stores models as named definitions and points roles at them by name, so switching a role no longer discards that model’s context window and modality overrides.

Definitions and roles arrived in netclaw 0.25.0. netclaw still reads the old inline shape and nothing rewrites it at startup, so upgrading changes nothing until you edit models. Migrate when you’re ready, or let the first model write do it for you.

  • An inline Models section in ~/.netclaw/config/netclaw.json. Open the file to check, because netclaw model list renders both shapes identically.
  • No legacy NETCLAW_Models__Main__*, __Fallback__*, or __Compaction__* variables exported anywhere the daemon can see them. They block migration outright, so convert them first.
  • A way to restart the daemon, if you have to convert environment variables (those need a manual restart).

Legacy - each role carries its own copy of the model:

{
"Models": {
"Main": { "Provider": "remote-gpu", "ModelId": "qwen3:30b", "ContextWindow": 32768 },
"Fallback": { "Provider": "remote-gpu", "ModelId": "qwen3:8b" }
}
}

Canonical - models are described once, roles reference them:

{
"Models": {
"Definitions": {
"remote-gpu-qwen3-30b": { "Provider": "remote-gpu", "ModelId": "qwen3:30b", "ContextWindow": 32768 },
"remote-gpu-qwen3-8b": { "Provider": "remote-gpu", "ModelId": "qwen3:8b" }
},
"Roles": {
"Main": "remote-gpu-qwen3-30b",
"Fallback": "remote-gpu-qwen3-8b"
}
}
}

A definition can also carry an optional MaxOutputTokens and Thinking level. Migration keeps a legacy role’s MaxOutputTokens, and netclaw model set --max-output-tokens and --thinking write them (see Models). Reusing a model with model set keeps the role’s own definition, so two definitions of one model, such as sonnet-low and sonnet-high, are fine.

Never both. netclaw won’t guess which one you meant, so a Models section holding inline roles and Definitions/Roles is rejected. See If you mix the two shapes.

ActionMigrates?
Upgrading netclawNo
Starting the daemonNo
netclaw model list, netclaw statusNo, reads don’t write
netclaw model setYes
netclaw model clearYes, when the role was actually set. Clearing an unset role writes nothing
The model-manager TUI assigning a roleYes
netclaw doctor --fixYes

Reading a legacy config leaves it byte-for-byte alone. The first write converts the whole Models section, not just the part you touched. We loaded a legacy file in a container, ran netclaw model list, and the file’s checksum didn’t change.

Run any model write, or do it deliberately:

Terminal window
netclaw doctor --fix -y

doctor --fix -y printed the planned diff, wrote the file, and created the backup in our test, and it also adds configVersion. Migration is deterministic. It keeps each role’s provider, model ID, context window, modalities, and provenance, and names definitions <provider>-<model-id> with punctuation in the model ID turned into hyphens (/models/Qwen3.8-27B.gguf becomes lab--models-qwen3-8-27b-gguf). Roles that pointed at identical models collapse into one shared definition:

{
"Models": {
"Definitions": {
"remote-gpu-qwen3-30b": { "Provider": "remote-gpu", "ModelId": "qwen3:30b", "ContextWindow": 32768 },
"remote-gpu-qwen3-8b": { "Provider": "remote-gpu", "ModelId": "qwen3:8b", "Provenance": "Manual" }
},
"Roles": {
"Main": "remote-gpu-qwen3-30b",
"Fallback": "remote-gpu-qwen3-8b",
"Compaction": "remote-gpu-qwen3-8b"
}
}
}

Fallback and Compaction share one definition here because they were the same model. Editing it now moves both. Split them into separate definitions if you want them to drift apart.

Confirm the result:

Terminal window
netclaw model list

The table should read exactly as it did before. The daemon restarts itself when netclaw.json changes, so there’s nothing to restart unless legacy environment variables were involved.

Migration first copies netclaw.json to ~/.netclaw/config/netclaw.json.legacy-models.bak - a complete snapshot of the file as it stood, not just the Models section. It’s never overwritten. If the name is taken, the next backup is netclaw.json.legacy-models.2.bak, then .3.bak, so a second migration can’t clobber your original.

An older netclaw binary can’t read the canonical shape. To downgrade, restore the backup before installing the old version:

Terminal window
cp ~/.netclaw/config/netclaw.json.legacy-models.bak ~/.netclaw/config/netclaw.json
netclaw model list # confirm the roles read as they did before

Legacy NETCLAW_Models__Main__* variables still work by themselves. But while any of them is set, migration stops with an error instead of proceeding, because after a restart you’d have a canonical file and legacy variables fighting over the same roles, which is the mixed shape netclaw rejects everywhere else.

Convert them first. Before:

Terminal window
export NETCLAW_Models__Main__Provider="openrouter"
export NETCLAW_Models__Main__ModelId="anthropic/claude-sonnet-4"
export NETCLAW_Models__Main__ContextWindow="200000"

After:

Terminal window
export NETCLAW_Models__Definitions__claude__Provider="openrouter"
export NETCLAW_Models__Definitions__claude__ModelId="anthropic/claude-sonnet-4"
export NETCLAW_Models__Definitions__claude__ContextWindow="200000"
export NETCLAW_Models__Roles__Main="claude"

The definition name (claude) is yours. It has to match on both sides. Unset the old variables before migrating. Check your shell profile, your Docker env file, and any Environment= or EnvironmentFile= lines in a systemd unit.

A Models section with Main (or Fallback, Compaction) next to Definitions and Roles is the one state netclaw refuses:

  • netclaw model list fails with Models configuration mixes legacy inline roles (Models:Main) with current keys (Models:Definitions, Models:Roles). Keep one set. and says which keys to remove. netclaw model set can’t repair it.
  • A running daemon ignores the edit and keeps its previous models. netclaw status reports config on disk not applied with the same message.
  • netclaw doctor fails Config Schema (/Models: oneOf: Expected 1 matching subschema but found 0), Chat Client, and Context Window.
  • A daemon that starts on the file stops with that one-line message and no model is served. Under the Docker image the entrypoint restarts it in a loop with growing backoff.

Delete whichever representation you don’t want, keep Definitions/Roles if it’s complete, and the next start works. Pasting a bare "Main": {...} snippet from an old guide into a canonical config is the usual way to end up here.

Legacy model roles conflict for <provider>/<model>

Section titled “Legacy model roles conflict for <provider>/<model>”

Two legacy roles name the same provider and model ID but disagree on metadata, such as different ContextWindow values. Migration can’t merge them into one definition and won’t pick a winner.

Make the roles agree, then migrate:

{
"Models": {
"Main": { "Provider": "remote-gpu", "ModelId": "qwen3:8b", "ContextWindow": 32768 },
"Fallback": { "Provider": "remote-gpu", "ModelId": "qwen3:8b", "ContextWindow": 32768 }
}
}

If they genuinely need different windows, they’re different definitions. Migrate by hand and give them separate names.

Cannot migrate Models while legacy environment override ... is set

Section titled “Cannot migrate Models while legacy environment override ... is set”

A NETCLAW_Models__Main__*, __Fallback__*, or __Compaction__* variable is still exported. Convert it and try again. Check the daemon’s environment too, not just your shell. Systemd units and Docker env files are the usual culprits. The error names the variable, for example Cannot migrate Models while legacy environment override 'NETCLAW_Models__Main__Provider' is set.

Models:Roles:<role> references unknown definition '<name>'

Section titled “Models:Roles:<role> references unknown definition '<name>'”

A role names a definition that isn’t there, usually after a rename. Migration doesn’t cause this and can’t fix it. Compare Roles against Definitions in netclaw.json and make the names match.

Models configuration mixes legacy inline roles with named Definitions/Roles

Section titled “Models configuration mixes legacy inline roles with named Definitions/Roles”

The Models section has both shapes. See If you mix the two shapes.

Models:<role> must explicitly declare Provider and ModelId before migration

Section titled “Models:<role> must explicitly declare Provider and ModelId before migration”

A legacy role relied on schema defaults instead of naming its provider and model. Write them out, then migrate.

Once the file is migrated, overrides live on the definition, so netclaw model set stops throwing away a model’s context window every time you reassign a role. Models covers the shape you now have.