fix: show config-managed custom providers

This commit is contained in:
Michael Lam
2026-05-20 06:22:46 -07:00
parent 9c983e693a
commit 8ef8fae831
4 changed files with 70 additions and 43 deletions
+3
View File
@@ -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)
+2
View File
@@ -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
+56 -43
View File
@@ -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;
+9
View File
@@ -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)