Skip to content

fix(router): cloud→local provider switch disables the router and resets tiers to openrouter #189

Description

@andreapn

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.

Metadata

Metadata

Assignees

Labels

area: providerLLM provider clients and provider compatibilityarea: routerPilot Router, tiers, model selection, and routing policyarea: testsTest infrastructure, fixtures, and CI verificationpriority: p2Medium priority: important but has a workaround or limited impactstatus: readyScoped and ready for implementationtype: bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions