mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(desktop): preserve configured base_url on same-provider model switch (#41121)
The desktop model picker calls POST /api/model/set with provider+model only (no base_url). _apply_main_model_assignment cleared model.base_url for every non-custom provider, so re-picking a Xiaomi MiMo model wiped a Token Plan endpoint (https://token-plan-*.xiaomimimo.com/v1) back to the registry default api.xiaomimimo.com — breaking valid tp- keys with 401s. Now base_url is cleared only when switching to a different provider (the stale URL belonged to the old one); same-provider re-assignment preserves it, and an explicitly supplied base_url is honored for any provider.
This commit is contained in:
parent
349a3f601c
commit
0507e4630d
2 changed files with 87 additions and 17 deletions
|
|
@ -692,23 +692,41 @@ def _apply_main_model_assignment(
|
|||
) -> dict:
|
||||
"""Apply a main-slot model assignment to a ``model`` config dict in place.
|
||||
|
||||
Sets ``provider``/``default``, then reconciles ``base_url``: custom/local
|
||||
providers persist the supplied endpoint URL (the runtime resolver reads
|
||||
``model.base_url`` from config and ignores ``OPENAI_BASE_URL``), while every
|
||||
other provider clears any stale URL so the resolver picks that provider's
|
||||
own default endpoint. The hardcoded ``context_length`` override is always
|
||||
dropped since the new model may have a different context window.
|
||||
Sets ``provider``/``default``, then reconciles ``base_url``:
|
||||
|
||||
- An explicitly supplied ``base_url`` is always persisted (covers
|
||||
``custom``/local endpoints and any provider whose key is bound to a
|
||||
non-default host).
|
||||
- Otherwise, a stale ``base_url`` is cleared ONLY when switching to a
|
||||
*different* provider — that URL belonged to the old provider. When the
|
||||
provider is unchanged and no new URL is supplied, the existing
|
||||
``base_url`` is preserved. This keeps a user's custom endpoint (e.g. a
|
||||
Xiaomi MiMo Token Plan host, ``https://token-plan-*.xiaomimimo.com/v1``)
|
||||
alive when they merely re-pick a model under the same provider — picking
|
||||
a model previously wiped it, forcing the registry default and breaking
|
||||
Token Plan keys.
|
||||
|
||||
The runtime resolver reads ``model.base_url`` from config (it ignores
|
||||
``OPENAI_BASE_URL``) and only honors it when the configured provider matches
|
||||
and the pool entry is on the registry default, so preserving it here is what
|
||||
lets the override actually route. The hardcoded ``context_length`` override
|
||||
is always dropped since the new model may have a different context window.
|
||||
|
||||
Returns the same dict (coerced to a fresh dict if the input wasn't one) so
|
||||
callers can assign it straight back onto ``cfg["model"]``.
|
||||
callers can assign it straight back onto the model config.
|
||||
"""
|
||||
if not isinstance(model_cfg, dict):
|
||||
model_cfg = {}
|
||||
prev_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||
new_provider = provider.strip().lower()
|
||||
model_cfg["provider"] = provider
|
||||
model_cfg["default"] = model
|
||||
if provider.strip().lower() == "custom" and base_url.strip():
|
||||
if base_url.strip():
|
||||
model_cfg["base_url"] = base_url.strip()
|
||||
elif model_cfg.get("base_url"):
|
||||
elif model_cfg.get("base_url") and new_provider != prev_provider:
|
||||
# Switching providers: the old URL belonged to the old provider, drop
|
||||
# it so the new provider's default endpoint is used. Same-provider
|
||||
# re-assignment keeps the user's configured base_url intact.
|
||||
model_cfg["base_url"] = ""
|
||||
model_cfg.pop("context_length", None)
|
||||
return model_cfg
|
||||
|
|
|
|||
|
|
@ -1269,10 +1269,11 @@ class TestWebServerEndpoints:
|
|||
assert data.get("gateway_tools", []) == []
|
||||
|
||||
def test_apply_main_model_assignment_base_url_and_context_reconcile(self):
|
||||
"""The shared main-slot assignment helper must persist base_url only for
|
||||
custom providers, clear stale base_url for hosted ones, and always drop
|
||||
a hardcoded context_length override. Both POST /api/model/set and
|
||||
profile-model writes route through this, so the contract is pinned here."""
|
||||
"""The shared main-slot assignment helper must persist a supplied
|
||||
base_url, clear a stale base_url only when switching providers, preserve
|
||||
it on same-provider re-assignment, and always drop a hardcoded
|
||||
context_length override. Both POST /api/model/set and profile-model
|
||||
writes route through this, so the contract is pinned here."""
|
||||
from hermes_cli.web_server import _apply_main_model_assignment
|
||||
|
||||
# Custom + base_url → persisted; stale context_length dropped.
|
||||
|
|
@ -1284,16 +1285,39 @@ class TestWebServerEndpoints:
|
|||
assert out["base_url"] == "http://127.0.0.1:8000/v1"
|
||||
assert "context_length" not in out
|
||||
|
||||
# Hosted provider → stale base_url cleared (no base_url supplied).
|
||||
# Switching providers (custom → openrouter) → stale base_url cleared.
|
||||
out = _apply_main_model_assignment(
|
||||
{"base_url": "http://127.0.0.1:8000/v1"}, "openrouter", "anthropic/claude-opus-4.8"
|
||||
{"provider": "custom", "base_url": "http://127.0.0.1:8000/v1"},
|
||||
"openrouter",
|
||||
"anthropic/claude-opus-4.8",
|
||||
)
|
||||
assert out["provider"] == "openrouter"
|
||||
assert out["base_url"] == ""
|
||||
|
||||
# Custom WITHOUT a base_url → don't invent one, clear any stale value.
|
||||
# Same provider, no new base_url → existing custom endpoint preserved.
|
||||
# Regression: picking a different MiMo model under xiaomi must NOT wipe a
|
||||
# Token Plan base_url (https://token-plan-*.xiaomimimo.com/v1).
|
||||
out = _apply_main_model_assignment(
|
||||
{"base_url": "http://stale:1/v1"}, "custom", "m"
|
||||
{"provider": "xiaomi", "base_url": "https://token-plan-ams.xiaomimimo.com/v1"},
|
||||
"xiaomi",
|
||||
"mimo-v2.5-pro",
|
||||
)
|
||||
assert out["provider"] == "xiaomi"
|
||||
assert out["default"] == "mimo-v2.5-pro"
|
||||
assert out["base_url"] == "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
|
||||
# A supplied base_url is honored for any provider, not just custom.
|
||||
out = _apply_main_model_assignment(
|
||||
{"provider": "xiaomi"},
|
||||
"xiaomi",
|
||||
"mimo-v2.5",
|
||||
"https://token-plan-cn.xiaomimimo.com/v1",
|
||||
)
|
||||
assert out["base_url"] == "https://token-plan-cn.xiaomimimo.com/v1"
|
||||
|
||||
# Switching providers without a base_url → don't invent one, clear stale.
|
||||
out = _apply_main_model_assignment(
|
||||
{"provider": "openrouter", "base_url": "http://stale:1/v1"}, "custom", "m"
|
||||
)
|
||||
assert out["base_url"] == ""
|
||||
|
||||
|
|
@ -1377,6 +1401,34 @@ class TestWebServerEndpoints:
|
|||
assert resp.status_code == 200
|
||||
assert resp.json()["base_url"] == ""
|
||||
|
||||
def test_set_model_main_same_provider_preserves_base_url(self):
|
||||
"""Re-picking a model under the SAME provider must NOT wipe a configured
|
||||
base_url. Regression for the desktop bug where selecting a Xiaomi MiMo
|
||||
model reset a Token Plan endpoint back to the registry default, breaking
|
||||
Token Plan keys (https://token-plan-*.xiaomimimo.com/v1)."""
|
||||
from hermes_cli.config import load_config, save_config
|
||||
|
||||
cfg = load_config()
|
||||
cfg["model"] = {
|
||||
"provider": "xiaomi",
|
||||
"default": "mimo-v2.5-pro",
|
||||
"base_url": "https://token-plan-ams.xiaomimimo.com/v1",
|
||||
}
|
||||
save_config(cfg)
|
||||
|
||||
# Desktop model picker sends provider+model only (no base_url).
|
||||
resp = self.client.post(
|
||||
"/api/model/set",
|
||||
json={"scope": "main", "provider": "xiaomi", "model": "mimo-v2.5"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["base_url"] == "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
|
||||
model_cfg = load_config().get("model")
|
||||
assert isinstance(model_cfg, dict)
|
||||
assert model_cfg["default"] == "mimo-v2.5"
|
||||
assert model_cfg["base_url"] == "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
|
||||
def test_set_model_main_reports_stale_auxiliary_pins(self):
|
||||
"""Switching the main provider must report auxiliary slots still pinned
|
||||
to a *different* provider so the UI can warn the user their helper tasks
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue