feat(desktop): custom endpoint settings (supersedes #42745) (#67759)

* feat(desktop): add custom endpoint settings (supersedes #42745)

Salvages PR #42745 (elashera:custom-endpoints-desktop), which could no
longer merge cleanly against main. Re-integrated the work onto current
main and reconciled the conflicts:

- Settings nav: wired the new 'Custom Endpoints' provider sub-view into
  main's data-driven navGroups/OverlayNav layout (PR predated that
  refactor) and added it to PROVIDER_VIEWS.
- providers-settings: kept BOTH main's LocalEndpointRow affordance and
  the PR's fuller CRUD panel; unified ProvidersSettingsProps to carry
  onClose + onConfigSaved + onMainModelChanged.
- web_server: kept main's _normalize_main_model_assignment + api_key
  propagation AND the PR's provider base_url lookup in
  _apply_model_assignment_sync.
- model_switch: dropped the PR's bare direct-custom-config picker block;
  main already implements it (source='model-config', with live model
  discovery). Updated the salvaged test to assert main's behavior.
- Merged additive import/type blocks in hermes.ts and types/hermes.ts.

Backend endpoints, i18n labels (en/ja/zh/zh-hant), and the
custom-endpoints-settings.tsx panel carried over. 28 custom-endpoint
tests pass.

Co-authored-by: elashera <emilio.jesus.lasheras.romero@nttdata.com>

* chore(contributors): map elashera's commit email

Salvage of #42745 (superseded by #67759) preserves @elashera's
authorship, whose corporate commit email had no contributor mapping.
Adds contributors/emails/ mapping so check-attribution passes.
Verified: GitHub user 'elashera' id=135239963 matches their own
noreply commit email (135239963+elashera@users.noreply.github.com).

---------

Co-authored-by: elashera <emilio.jesus.lasheras.romero@nttdata.com>
This commit is contained in:
Austin Pickett 2026-07-19 19:59:46 -04:00 • committed by GitHub
parent 57063ad47f
commit 3d97893571
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 902 additions and 5 deletions

View file

@ -4188,6 +4188,101 @@ class TestWebServerEndpoints:
assert model_cfg["provider"] == "openrouter"
assert model_cfg.get("base_url", "") == ""
def test_custom_endpoints_list_includes_direct_custom_config(self):
"""A bare model.provider=custom config should show up in Desktop even
before the user has materialized it under providers.
"""
from hermes_cli.config import save_config
save_config({
"model": {
"provider": "custom",
"default": "gpt-5.4",
"base_url": "http://127.0.0.1:8081/v1",
"api_key": "sk-local",
},
"providers": {},
})
resp = self.client.get("/api/providers/custom-endpoints")
assert resp.status_code == 200
data = resp.json()
assert data["current"] == {
"provider": "custom",
"model": "gpt-5.4",
"base_url": "http://127.0.0.1:8081/v1",
}
assert data["endpoints"][0]["id"] == "custom"
assert data["endpoints"][0]["source"] == "direct-config"
assert data["endpoints"][0]["has_api_key"] is True
def test_custom_endpoint_upsert_persists_provider_and_sets_default(self):
"""Desktop can persist an OpenAI-compatible proxy in providers and make
it the default for new chats.
"""
from hermes_cli.config import load_config
resp = self.client.post(
"/api/providers/custom-endpoints",
json={
"id": "axet-proxy",
"name": "Axet Proxy",
"base_url": "http://127.0.0.1:8081/v1/",
"model": "gpt-5.4",
"api_key": "sk-local",
"context_length": 262144,
"make_default": True,
},
)
assert resp.status_code == 200
data = resp.json()
assert data["ok"] is True
assert data["id"] == "axet-proxy"
endpoint = next(e for e in data["endpoints"] if e["id"] == "axet-proxy")
assert endpoint["base_url"] == "http://127.0.0.1:8081/v1"
assert endpoint["model"] == "gpt-5.4"
assert endpoint["is_current"] is True
cfg = load_config()
assert cfg["providers"]["axet-proxy"]["base_url"] == "http://127.0.0.1:8081/v1"
assert cfg["providers"]["axet-proxy"]["models"]["gpt-5.4"]["context_length"] == 262144
assert cfg["model"]["provider"] == "axet-proxy"
assert cfg["model"]["default"] == "gpt-5.4"
assert cfg["model"]["base_url"] == "http://127.0.0.1:8081/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.
"""
from hermes_cli.config import load_config, save_config
save_config({
"model": {"provider": "nous", "default": "hermes-4"},
"providers": {
"axet-proxy": {
"name": "Axet Proxy",
"base_url": "http://127.0.0.1:8081/v1",
"api_key": "sk-local",
"model": "gpt-5.4",
"models": {"gpt-5.4": {}},
}
},
})
resp = self.client.post(
"/api/model/set",
json={"scope": "main", "provider": "axet-proxy", "model": "gpt-5.4"},
)
assert resp.status_code == 200
model_cfg = load_config()["model"]
assert model_cfg["provider"] == "axet-proxy"
assert model_cfg["default"] == "gpt-5.4"
assert model_cfg["base_url"] == "http://127.0.0.1:8081/v1"
assert model_cfg["api_key"] == "sk-local"
def test_set_model_main_gateway_failure_does_not_block_save(self, monkeypatch):
"""A Portal/gateway hiccup must never prevent saving the model."""
import hermes_cli.nous_subscription as ns