{"record":{"id":"c2772f36066bb4e3","repo":"NousResearch/hermes-agent","slug":"switch-model-no-base-url-resolved-for-provider","errorCode":null,"errorMessage":"switch_model: no base_url resolved for provider '{new_provider}' (switching from '{old_provider}'); refusing to keep the previous provider's endpoint","messagePattern":"switch_model: no base_url resolved for provider '(.+?)' \\(switching from '(.+?)'\\); refusing to keep the previous provider's endpoint","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/agent_runtime_helpers.py","lineNumber":2583,"sourceCode":"        # provider is actually changing, do NOT fall back to the current\n        # (old provider's) URL — that silently pairs the new provider label\n        # with the previous provider's endpoint (e.g. new_provider=minimax\n        # paired with the leftover api.githubcopilot.com URL), and every\n        # request after the switch 400s at the wrong host. This mismatched\n        # pair also gets snapshotted into _primary_runtime below, so it\n        # keeps re-applying on every subsequent turn until a full restart.\n        # Fail loud instead: the caller (model_switch.switch_model())\n        # already resolves base_url for every real provider, so an empty\n        # value here means resolution failed upstream, not that the\n        # provider genuinely has none. Re-selecting the SAME provider with\n        # an empty base_url (e.g. a credential-only refresh) is still fine\n        # to keep the current URL. See #47828.\n        old_norm_provider = (old_provider or \"\").strip().lower()\n        new_norm_provider = (new_provider or \"\").strip().lower()\n        if base_url:\n            agent.base_url = base_url\n        elif old_norm_provider != new_norm_provider:\n            raise ValueError(\n                f\"switch_model: no base_url resolved for provider \"\n                f\"'{new_provider}' (switching from '{old_provider}'); \"\n                \"refusing to keep the previous provider's endpoint\"\n            )\n        agent.api_mode = api_mode\n        # Invalidate transport cache — new api_mode may need a different transport\n        if hasattr(agent, \"_transport_cache\"):\n            agent._transport_cache.clear()\n        if api_key:\n            agent.api_key = api_key\n\n        # ── Reload credential pool for the new provider (issue #52727) ──\n        # Without this, ``recover_with_credential_pool`` sees a\n        # ``pool.provider != agent.provider`` mismatch and short-circuits,\n        # leaving the new provider with no rotation/recovery on 401/429 and\n        # burning the original pool's entries. Only reload when the provider\n        # actually changed (or the pool was missing) — re-selecting the same\n        # provider must not churn the pool reference. A reload failure is","sourceCodeStart":2565,"sourceCodeEnd":2601,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/agent_runtime_helpers.py#L2565-L2601","documentation":"switch_model refuses to change providers when the new provider's base_url resolves empty. Keeping the previous provider's endpoint would silently send provider B's traffic to provider A's URL, and (per the comment citing #47828) the mis-applied state would persist on every subsequent turn until restart. Re-selecting the SAME provider with an empty base_url (credential-only refresh) is still allowed.","triggerScenarios":"model_switch.switch_model() reaches this helper with new_provider differing from old_provider (case-insensitive) while base_url is empty/None — provider-profile resolution produced no default endpoint and none was passed in.","commonSituations":"A custom provider plugin registered without a default base_url; provider name typo after a plugin rename; config resolving the provider credential-only with no endpoint; provider plugin not yet discovered at switch time.","solutions":["Ensure the target provider's profile (plugins/model-providers/<name>) defines a base_url, or supply model.base_url in config.yaml","Check the provider name for typos in the `hermes model` selection","If the switch was meant as a credential-only refresh, keep the provider identical instead of naming a different one","Update the provider plugin if its ProviderProfile shape changed"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"def can_switch(old_provider: str, new_provider: str, new_base_url) -> bool:\n    if new_base_url:\n        return True\n    return (old_provider or \"\").strip().lower() == (new_provider or \"\").strip().lower()\n\nassert can_switch(old_provider, new_provider, resolved_base_url), (\n    f\"refusing switch to {new_provider!r}: no base_url resolved\"\n)","typeGuard":null,"tryCatchPattern":"try:\n    switch_model(new_provider=new_provider, base_url=resolved_url)\nexcept ValueError as e:\n    if \"no base_url resolved\" in str(e):\n        abort_switch_and_keep_current()  # current endpoint stays valid\n    else:\n        raise","preventionTips":["Resolve and pass an explicit base_url for every provider switch","Ensure provider profiles (plugins/model-providers/<name>) always declare a default base_url","Double-check provider spelling in switch commands — unknown names resolve no endpoint"],"tags":["model-switch","provider","base-url","config"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}