mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fix(dashboard): clear the model mirror when its custom endpoint is deleted
activate_custom_endpoint copies the endpoint's base_url and api_key onto
cfg["model"]. delete_custom_endpoint pops the providers entry and saves —
it never touches that mirror.
So deleting the endpoint the agent is currently using leaves both behind:
DELETE /api/providers/custom-endpoints/acme -> 200
providers entry gone : True
model.api_key : sk-CUSTOM-ENDPOINT-SECRET
model.base_url : https://llm.acme.corp/v1
Two consequences, both silent:
* The agent keeps authenticating to the deleted host with the deleted key.
model.api_key outranks the environment at client construction, so this
also shadows whatever the operator configures next — the persistent-401
shape credential_lifecycle.py documents as #62269.
* A credential the operator just removed through the dashboard stays
sitting in config.yaml.
Scrub the main-slot mirror on delete, but only when it actually names the
deleted provider — an endpoint deleted while a different one is active must
leave that active assignment untouched. Both directions are pinned by tests.
This commit is contained in:
parent
6bedec4734
commit
b520f507cc
2 changed files with 87 additions and 0 deletions
|
|
@ -7037,6 +7037,29 @@ def _custom_endpoint_response(cfg: Dict[str, Any]) -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _detach_main_model_from_provider(cfg: Dict[str, Any], provider_key: str) -> None:
|
||||
"""Drop the main-slot mirror of a provider that no longer exists.
|
||||
|
||||
``activate_custom_endpoint`` copies the endpoint's ``base_url`` and
|
||||
``api_key`` onto ``model``. That mirror outranks the environment at client
|
||||
construction (#62269), so deleting the endpoint without clearing it leaves
|
||||
the agent still authenticating to the deleted host with the deleted key —
|
||||
and leaves that key sitting in config.yaml after the operator believes the
|
||||
dashboard removed it.
|
||||
|
||||
Only touches ``model`` when it actually names the deleted provider, so an
|
||||
endpoint deleted while a *different* provider is active is left alone.
|
||||
"""
|
||||
model_cfg = cfg.get("model")
|
||||
if not isinstance(model_cfg, dict):
|
||||
return
|
||||
if str(model_cfg.get("provider") or "").strip().lower() != provider_key:
|
||||
return
|
||||
for field in ("provider", "base_url", "api_key"):
|
||||
model_cfg.pop(field, None)
|
||||
cfg["model"] = model_cfg
|
||||
|
||||
|
||||
def _write_custom_endpoint(cfg: Dict[str, Any], body: CustomEndpointUpdate) -> Tuple[str, Dict[str, Any]]:
|
||||
endpoint_id = _custom_endpoint_id(body.id or body.name)
|
||||
name = (body.name or "").strip()
|
||||
|
|
@ -7170,6 +7193,7 @@ def delete_custom_endpoint(endpoint_id: str):
|
|||
raise HTTPException(status_code=404, detail="custom endpoint not found")
|
||||
providers.pop(provider_key, None)
|
||||
cfg["providers"] = providers
|
||||
_detach_main_model_from_provider(cfg, provider_key)
|
||||
save_config(cfg)
|
||||
response = _custom_endpoint_response(cfg)
|
||||
response["ok"] = True
|
||||
|
|
|
|||
|
|
@ -4389,6 +4389,69 @@ class TestWebServerEndpoints:
|
|||
assert sorted(models) == ["acme/model-1", "acme/model-2"]
|
||||
assert models["acme/model-1"]["context_length"] == 200000
|
||||
|
||||
def test_deleting_the_active_custom_endpoint_clears_its_model_mirror(self):
|
||||
"""Deleting an endpoint must not leave its key running the agent.
|
||||
|
||||
``activate`` copies the endpoint's base_url + api_key onto ``model``,
|
||||
and ``model.api_key`` outranks the environment at client construction
|
||||
(#62269). Without clearing that mirror the agent keeps authenticating
|
||||
to the deleted host with the deleted key, and the key the operator
|
||||
just removed through the dashboard stays in config.yaml.
|
||||
"""
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
self.client.post(
|
||||
"/api/providers/custom-endpoints",
|
||||
json={
|
||||
"id": "acme",
|
||||
"name": "Acme",
|
||||
"base_url": "https://llm.acme.corp/v1",
|
||||
"model": "acme/model-1",
|
||||
"api_key": "sk-acme-secret",
|
||||
},
|
||||
)
|
||||
assert self.client.post(
|
||||
"/api/providers/custom-endpoints/acme/activate", json={}
|
||||
).status_code == 200
|
||||
|
||||
cfg = load_config()
|
||||
assert cfg["model"]["api_key"] == "sk-acme-secret"
|
||||
|
||||
assert self.client.request(
|
||||
"DELETE", "/api/providers/custom-endpoints/acme"
|
||||
).status_code == 200
|
||||
|
||||
cfg = load_config()
|
||||
assert "acme" not in (cfg.get("providers") or {})
|
||||
model_cfg = cfg.get("model") or {}
|
||||
assert not model_cfg.get("api_key"), "deleted endpoint's key still in config.yaml"
|
||||
assert not model_cfg.get("base_url"), "deleted endpoint's host still routed to"
|
||||
assert not model_cfg.get("provider")
|
||||
|
||||
def test_deleting_an_inactive_custom_endpoint_leaves_the_active_one_alone(self):
|
||||
"""Only the mirror of the DELETED provider is scrubbed."""
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
for name, key in (("acme", "sk-acme"), ("other", "sk-other")):
|
||||
self.client.post(
|
||||
"/api/providers/custom-endpoints",
|
||||
json={
|
||||
"id": name,
|
||||
"name": name,
|
||||
"base_url": f"https://llm.{name}.corp/v1",
|
||||
"model": f"{name}/m",
|
||||
"api_key": key,
|
||||
},
|
||||
)
|
||||
|
||||
self.client.post("/api/providers/custom-endpoints/other/activate", json={})
|
||||
self.client.request("DELETE", "/api/providers/custom-endpoints/acme")
|
||||
|
||||
model_cfg = load_config().get("model") or {}
|
||||
assert model_cfg.get("provider") == "other"
|
||||
assert model_cfg.get("api_key") == "sk-other"
|
||||
assert model_cfg.get("base_url") == "https://llm.other.corp/v1"
|
||||
|
||||
def test_set_model_main_preserves_base_url_for_named_custom_provider(self):
|
||||
"""Selecting a named custom endpoint from the Desktop model picker
|
||||
should keep its endpoint URL attached to model config.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue