From 8ef8fae8317ebf6b44128925ed78ee8ce4fbf2e7 Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Wed, 20 May 2026 06:22:46 -0700 Subject: [PATCH] fix: show config-managed custom providers --- CHANGELOG.md | 3 + api/providers.py | 2 + static/panels.js | 99 ++++++++++++++----------- tests/test_custom_providers_in_panel.py | 9 +++ 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 185f15f1..38b21f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Fixed +- **PR #2634** by @Michaelyklam (closes #2632) — Show `custom_providers` entries created by `hermes model` in Settings → Providers as read-only config-managed provider cards, including their configured models and key status, instead of filtering them out because they are not WebUI-editable API-key providers. + ## [v0.51.95] — 2026-05-20 — Release BS (stage-388 — 5-PR batch — live tool callback event dedup + browser-only dashboard links + messaging transcript merge alignment + Geist Contrast skin + SSE runtime diagnostics) diff --git a/api/providers.py b/api/providers.py index 93233ce4..dc8066dc 100644 --- a/api/providers.py +++ b/api/providers.py @@ -1984,8 +1984,10 @@ def get_providers() -> dict[str, Any]: "display_name": cp_name, "has_key": cp_has_key, "configurable": False, # custom providers managed via config.yaml + "is_custom": True, "key_source": "config_yaml" if cp_has_key else "none", "models": cp_models, + "models_total": len(cp_models), }) # Determine active provider diff --git a/static/panels.js b/static/panels.js index b40252e7..a716dcef 100644 --- a/static/panels.js +++ b/static/panels.js @@ -5770,7 +5770,7 @@ async function loadProvidersPanel(){ try{ const data=await api('/api/providers'); const quota=await _fetchProviderQuotaStatus(false).catch(e=>({ok:false,status:'unavailable',quota:null,message:e.message||t('provider_quota_unavailable'),client_fetched_at:new Date().toISOString()})); - const providers=(data.providers||[]).filter(p=>p.configurable||p.is_oauth); + const providers=(data.providers||[]).filter(p=>p.configurable||p.is_oauth||p.is_custom); list.innerHTML=''; _providerCardEls.clear(); const quotaCard=_buildProviderQuotaCard(quota); @@ -6097,48 +6097,59 @@ function _buildProviderCard(p){ return card; } - const field=document.createElement('div'); - field.className='provider-card-field'; - const label=document.createElement('label'); - label.className='provider-card-label'; - label.textContent=t('providers_status_api_key'); - field.appendChild(label); + let input=null; + let saveBtn=null; + if(p.configurable){ + const field=document.createElement('div'); + field.className='provider-card-field'; + const label=document.createElement('label'); + label.className='provider-card-label'; + label.textContent=t('providers_status_api_key'); + field.appendChild(label); - const row=document.createElement('div'); - row.className='provider-card-row'; - const input=document.createElement('input'); - input.type='password'; - input.className='provider-card-input'; - input.placeholder=p.has_key?t('providers_key_placeholder_replace'):t('providers_key_placeholder_new'); - input.autocomplete='off'; - const toggleBtn=document.createElement('button'); - toggleBtn.type='button'; - toggleBtn.className='provider-card-btn provider-card-btn-ghost'; - toggleBtn.textContent='Show'; - toggleBtn.onclick=()=>{ - const revealed=input.type==='text'; - input.type=revealed?'password':'text'; - toggleBtn.textContent=revealed?'Show':'Hide'; - }; - const saveBtn=document.createElement('button'); - saveBtn.type='button'; - saveBtn.className='provider-card-btn provider-card-btn-primary'; - saveBtn.textContent=t('providers_save'); - saveBtn.onclick=()=>_saveProviderKey(p.id); - saveBtn.disabled=true; - row.appendChild(input); - row.appendChild(toggleBtn); - row.appendChild(saveBtn); - if(p.has_key){ - const removeBtn=document.createElement('button'); - removeBtn.type='button'; - removeBtn.className='provider-card-btn provider-card-btn-danger'; - removeBtn.textContent=t('providers_remove'); - removeBtn.onclick=()=>_removeProviderKey(p.id); - row.appendChild(removeBtn); + const row=document.createElement('div'); + row.className='provider-card-row'; + input=document.createElement('input'); + input.type='password'; + input.className='provider-card-input'; + input.placeholder=p.has_key?t('providers_key_placeholder_replace'):t('providers_key_placeholder_new'); + input.autocomplete='off'; + const toggleBtn=document.createElement('button'); + toggleBtn.type='button'; + toggleBtn.className='provider-card-btn provider-card-btn-ghost'; + toggleBtn.textContent='Show'; + toggleBtn.onclick=()=>{ + const revealed=input.type==='text'; + input.type=revealed?'password':'text'; + toggleBtn.textContent=revealed?'Show':'Hide'; + }; + saveBtn=document.createElement('button'); + saveBtn.type='button'; + saveBtn.className='provider-card-btn provider-card-btn-primary'; + saveBtn.textContent=t('providers_save'); + saveBtn.onclick=()=>_saveProviderKey(p.id); + saveBtn.disabled=true; + row.appendChild(input); + row.appendChild(toggleBtn); + row.appendChild(saveBtn); + if(p.has_key){ + const removeBtn=document.createElement('button'); + removeBtn.type='button'; + removeBtn.className='provider-card-btn provider-card-btn-danger'; + removeBtn.textContent=t('providers_remove'); + removeBtn.onclick=()=>_removeProviderKey(p.id); + row.appendChild(removeBtn); + } + field.appendChild(row); + body.appendChild(field); + }else{ + const hint=document.createElement('div'); + hint.className='provider-card-hint'; + hint.textContent=p.is_custom + ? 'Custom provider loaded from config.yaml / hermes model. Edit it from the CLI or config file.' + : 'Provider is managed outside the WebUI.'; + body.appendChild(hint); } - field.appendChild(row); - body.appendChild(field); // Model list — show when provider has known models if(modelCount>0){ @@ -6192,8 +6203,10 @@ function _buildProviderCard(p){ body.appendChild(refreshRow); card.appendChild(body); - _providerCardEls.set(p.id,{card,input,saveBtn,hasKey:p.has_key}); - input.addEventListener('input',()=>{saveBtn.disabled=!input.value.trim();}); + if(input&&saveBtn){ + _providerCardEls.set(p.id,{card,input,saveBtn,hasKey:p.has_key}); + input.addEventListener('input',()=>{saveBtn.disabled=!input.value.trim();}); + } header.addEventListener('click',e=>{ // Don't toggle when clicking inside body (defensive; body isn't inside header) if(e.target.closest('.provider-card-body')) return; diff --git a/tests/test_custom_providers_in_panel.py b/tests/test_custom_providers_in_panel.py index 1253a068..1cf7c750 100644 --- a/tests/test_custom_providers_in_panel.py +++ b/tests/test_custom_providers_in_panel.py @@ -91,6 +91,7 @@ class TestCustomProvidersInGetProviders: assert glmcode["configurable"] is False, ( "custom providers should not be configurable via WebUI" ) + assert glmcode["is_custom"] is True assert glmcode["key_source"] == "config_yaml" assert glmcode["display_name"] == "glmcode" @@ -99,9 +100,17 @@ class TestCustomProvidersInGetProviders: assert "glm-5.1" in model_ids, ( f"Expected glm-5.1 in models, got: {model_ids}" ) + assert glmcode["models_total"] == 1 finally: self._restore_cfg(old_cfg, old_mtime) + def test_providers_panel_renders_config_yaml_custom_providers(self): + """Settings → Providers must not filter out read-only custom providers.""" + src = open("static/panels.js", encoding="utf-8").read() + assert "filter(p=>p.configurable||p.is_oauth||p.is_custom)" in src + assert "Custom provider loaded from config.yaml / hermes model" in src + assert "if(p.configurable){" in src + def test_custom_provider_with_multi_models(self, monkeypatch, tmp_path): """Custom provider with `models` list should expose all entries.""" _install_fake_hermes_cli(monkeypatch)