Summary
Switching the LLM provider cloud → local leaves the Pilot Router disabled and its tiers pinned to a provider that is not configured. A first-time local setup does not — same end state, different result.
Split out of #188, which covers the broader "preserve provider configuration across switches" request. This part is a standalone defect.
Reproduction
from agentos.gateway.config import GatewayConfig
from agentos.onboarding.mutations import upsert_llm_provider
c = upsert_llm_provider(GatewayConfig(), provider_id="ollama", model="qwen3.5:9b").config
# enabled=True profile=None c0={'provider': 'ollama', 'model': 'qwen3.5:9b', ...}
c = upsert_llm_provider(c, provider_id="deepseek", model="deepseek-chat",
api_key_env="DEEPSEEK_API_KEY").config
# enabled=True profile='deepseek' c0={'provider': 'deepseek', ...}
c = upsert_llm_provider(c, provider_id="ollama", model="qwen3.5:9b").config
# enabled=False profile=None c0={'provider': 'openrouter', 'model': 'deepseek/deepseek-v4-flash', ...}
Expected
Step 3 should match step 1: router enabled=True, tiers pinned to ollama / qwen3.5:9b.
Actual
agentos_router.enabled flips to False
- Tiers reset to the shipped openrouter defaults, so every tier names a provider that has no credentials while
llm.provider is ollama
Cause
_reconcile_router_profile_for_provider in src/agentos/onboarding/mutations.py:155-218.
The local-pinning branch is gated on not current_profile:
if is_local_provider(provider_id) and not current_profile:
... # pins every tier to provider+model
On a first-time local setup tier_profile is None, so the branch runs. After a hop through any provider in ROUTER_TIER_PROFILE_IDS, tier_profile is set, so the branch is skipped and execution falls through to mutations.py:195-201:
router_payload.pop("tiers", None)
if provider_id in ROUTER_TIER_PROFILE_IDS:
router_payload["tier_profile"] = provider_id
else:
router_payload["enabled"] = False
router_payload["tier_profile"] = None
ollama is not a profile id, so the router is disabled and tiers fall back to the shipped defaults.
Suggested fix
Drop current_profile from the local-provider guard, or clear tier_profile before the local check, so a local provider always takes the local-pinning path regardless of the previously active profile.
Test gap
tests/test_onboarding/test_mutations.py covers local→local (test_local_to_local_switch_repins_machine_written_tiers:468) and local→cloud (test_local_to_cloud_switch_restores_tier_profile:501), but there is no cloud→local case. A regression test for that direction should land with the fix.
Summary
Switching the LLM provider cloud → local leaves the Pilot Router disabled and its tiers pinned to a provider that is not configured. A first-time local setup does not — same end state, different result.
Split out of #188, which covers the broader "preserve provider configuration across switches" request. This part is a standalone defect.
Reproduction
Expected
Step 3 should match step 1: router
enabled=True, tiers pinned toollama/qwen3.5:9b.Actual
agentos_router.enabledflips toFalsellm.providerisollamaCause
_reconcile_router_profile_for_providerinsrc/agentos/onboarding/mutations.py:155-218.The local-pinning branch is gated on
not current_profile:On a first-time local setup
tier_profileisNone, so the branch runs. After a hop through any provider inROUTER_TIER_PROFILE_IDS,tier_profileis set, so the branch is skipped and execution falls through tomutations.py:195-201:ollamais not a profile id, so the router is disabled and tiers fall back to the shipped defaults.Suggested fix
Drop
current_profilefrom the local-provider guard, or cleartier_profilebefore the local check, so a local provider always takes the local-pinning path regardless of the previously active profile.Test gap
tests/test_onboarding/test_mutations.pycovers local→local (test_local_to_local_switch_repins_machine_written_tiers:468) and local→cloud (test_local_to_cloud_switch_restores_tier_profile:501), but there is no cloud→local case. A regression test for that direction should land with the fix.